0. 写在前面
要很好地使用一门语言,基础性的东西越了解、本质性的东西越熟悉,使用过程中也就必然越顺手,同时遇到各种坑各种问题也能很快找到方法很快解决。
本篇是本人学习linux c的学习笔记,所以不会从基础的语法层面来介绍C。在读本文的时候,已经假定你至少非常熟悉一门C系语言(如:java,c#或者php),并且对C也有一个初步的认知。
因些,本文重点是对C语言常用的一些概念进行记录总结。本篇主要介绍的概念有:
- 预处理器
- 结构体
- 指针
1. 从预处理说起
为什么先说预处理,一方面是方面是因为几乎所有的C代码第一行都是预处理开始的,而且预处理也是C语言中使用最频繁使用的。可以如果不理解预处理那代码就几乎没办法看。
1.1 什么是预处理
预处理就以#开头的语句,然后后面带对应的预处理指令如#define ,#include,#ifdef ....
用最简单的话来说,预处理就一个堆占位符,编译器会根据预处理指定替换成对应的内容。比如,使用inlcude的时候,就会把对应的头文件替换成预处理标签。
1.2 include包含头文件
这个大家都熟悉了,写c第一句就是#include <stdio.h> 。可以简单理解,当编译器遇到这个预处理语句的时候,就会对对应的头文件包含在代码叫,然后在代码中就可以使用声明的宏、函数或全局变量了。
include 预处理有#include <xx.h>和#include "xx.h"两种类型,这种个类型是有区别的使用<>可以理解为去系统环境中查找头文件,而""则是在当前源文件目录去查询。
能理解这两个基本OK。
1.3 define定义宏
这个是重中之重,宏很多人理解为常量,其实是完全完全错误的。
因为宏的定义是很复杂的,不仅可以定义变量,还可以定义代码块,甚至带参数的的宏都是可以定义的。
理解宏最好的方式就是把宏,理解为一种可以替换的标签了。编译器,会把把宏标签替换成对应的内容,然后进行编译最终生成可执行代码。
废话少说,上代码(先思考一下代码的执行结果):
#include <stdio.h>
// 最经典定义宏的例子
// 但要注意的是在引用PI的时候,其实就是相当于写了3.1415926这个内容
#define PI 3.1415926
//定义一个带参数的 宏,当使用MAX(3,4) 实际上相当于写了代码: 3 > 4 ? 3 :4
#define MAX(x,y) x > y ? x : y
//定义多行代码的宏,每行使用\分隔
//当然少不了我最喜欢的九九乘法表
#define NN_TABLE for (int nn_i = 1; nn_i <10; ++nn_i) { \
for (int nn_j = 1; nn_j <=nn_i; ++nn_j) { \
printf("%d*%d=%d ",nn_i,nn_j,nn_i*nn_j); \
} \
printf("\n"); \
}
//你没看错,这样的定义是完全可以的
//还定义一个表达式宏
// 试想一个DEP_EXP *2 值为多少?14?
#define DEF_EXP 3 + 4
int main() {
printf("PI Is %f\n",PI);
int i =2,j=8;
printf("%d,%d max is :%d\n",i,j,MAX(i,j));
NN_TABLE
int k = DEF_EXP *i ;
printf("k is :%d\n",k);
printf("-------------------\n");
NN_TABLE
}
运行结果是:
PI Is 3.141593
2,8 max is :8
1*1=1
2*1=2 2*2=4
3*1=3 3*2=6 3*3=9
4*1=4 4*2=8 4*3=12 4*4=16
5*1=5 5*2=10 5*3=15 5*4=20 5*5=25
6*1=6 6*2=12 6*3=18 6*4=24 6*5=30 6*6=36
7*1=7 7*2=14 7*3=21 7*4=28 7*5=35 7*6=42 7*7=49
8*1=8 8*2=16 8*3=24 8*4=32 8*5=40 8*6=48 8*7=56 8*8=64
9*1=9 9*2=18 9*3=27 9*4=36 9*5=45 9*6=54 9*7=63 9*8=72 9*9=81
k is :11
1*1=1
2*1=2 2*2=4
3*1=3 3*2=6 3*3=9
4*1=4 4*2=8 4*3=12 4*4=16
5*1=5 5*2=10 5*3=15 5*4=20 5*5=25
6*1=6 6*2=12 6*3=18 6*4=24 6*5=30 6*6=36
7*1=7 7*2=14 7*3=21 7*4=28 7*5=35 7*6=42 7*7=49
8*1=8 8*2=16 8*3=24 8*4=32 8*5=40 8*6=48 8*7=56 8*8=64
9*1=9 9*2=18 9*3=27 9*4=36 9*5=45 9*6=54 9*7=63 9*8=72 9*9=81
-------------------
1*1=1
2*1=2 2*2=4
3*1=3 3*2=6 3*3=9
4*1=4 4*2=8 4*3=12 4*4=16
5*1=5 5*2=10 5*3=15 5*4=20 5*5=25
6*1=6 6*2=12 6*3=18 6*4=24 6*5=30 6*6=36
7*1=7 7*2=14 7*3=21 7*4=28 7*5=35 7*6=42 7*7=49
8*1=8 8*2=16 8*3=24 8*4=32 8*5=40 8*6=48 8*7=56 8*8=64
9*1=9 9*2=18 9*3=27 9*4=36 9*5=45 9*6=54 9*7=63 9*8=72 9*9=81
如果能理解这段代码,基本上对宏能有一个大概的了解了,至少一般代码查看是没问题了。
如果理解不了?那多看几遍呗。
1.4 ifdef 预定处理定义
#include <stdio.h>
#define IS_WINDOWS
int main(){
#ifdef IS_WINDOWS
printf("在windows操作系统中");
#endif
return 0;
}
可以注释或者去年 #define IS_WINDOWS看一下效果
1.5 其它预处理指令
其它的还有#undef,#ifndef,#if #elif,#else,#error等错误,大概也能看字理解,如果不能理解。。。那就查看资料了
2. 结构(struct)
结构也是C语言很重要的一个概念,可以把结构理解为一组数组的集合。如果熟悉其它语言的话,可以把结构理解为一种只有属性没有方法的类。
2.1 结构定义
struct Person{ //1.定义一个结构体 ,可选
unsigned int age; //2.定义成员
unsigned int gender;
char name[50];
}person; //3.顺便定义一个结构体变量,可选
定义一个结构是很简单的,使用struct定义即可,使用也是很简单的
person.age = 20; //person是在定义结构的时候定义的,相当于一个全局变量
person.gender = 1;
strcpy( person.name,"张三");
printf("name:%s,age:%d,gener:%d\n",person.name ,person.age,person.gender);
struct Person p2 ; //定义一个新的结构
strcpy( p2.name,"李四");
printf("name:%s,age:%d,gener:%d\n",p2.name ,p2.age,p2.gender);
2.2 typedef定义结构
如果不仅仅只是想定义一个类型,则可以使用typedef来定义结构
typedef struct [Person]{ //1.定义一个结构体 ,可选
unsigned int age; //2.定义成员
unsigned int gender;
char name[50];
}PERSON; //3.定义类型别名
就可以使用
PERSON p; //这种方式来声明变量了
总的来说,结构的应该还是比如好理解的,把他看成一种定义的复杂类型即可。只是和其它面向对象的语言来讲,没办法直接定义成员函数(其实通过函数指针也可以实现,后面会介绍)。
3. 指针
3.1 指针概念
指针在C里面,是一个非常非常重要的概念,也是C里面的一非常难的一点。
首先要明白,什么是指针。指针可以简单的理解成一种特殊的变量,只是这种变量存储的是另外一个变量的内存地址。在C语言中,可以很方便的获取到一个变量的内存地址(指针),或从一个指针中获取/设置值,甚至操作一个移动操作指针。
指针的概念是很简单的,也容易理解。但如果不当使用指针,会出现各类的问题(如内存泄漏,内存溢出漏洞等),所以在使用指针前最好先焚香沐浴更衣而且得有个好人品,当然最重要的是明白下面几点:
- 对计算机内存模型需要有一个概念,知道数据是怎么在内在中分布的。(后面章节会做出说明)
- 要明白C的执行效率高,内存利用率高一方面是C直接编译成机器语言。另外一方面就是你可以自由的申请、操作、销毁内存。
- C执行效率高的代价就是你自己需要管理内存,所以在编程的时候你必须时刻都要理解当前程序执行时候的内存结构,最合理的申请内存,内存使用后进行释放,养成很好的编程习惯。
- 最后,要明白是世界上没有银弹,在编程界更是如此。没有最好的编程语言,只有最合适的编程语言,能以最低的成本解决问题才是最重要的.
3.2 内存模型概念
计算机是一个内存是一个非常大的概念,关联了硬件、CPU再到操作系统。一个章节是没办法全部讲明白了,当然了本人也也没 那么高的水平。本章节,只是基于C语言对指针操作的需求,对内存的概念做一下简单的说明,如果描述错误或不太准确的地方可以在评论中提出。
我们第一节计算机课就明白了一个公式即:
计算机程序=代码+数据
说白了程序就是通过代码操作(执行操作指令的自然是CPU)一些数据,并最终得到一个处理结果。
在编程过程中,逻辑部分已经由高级语言封装好了(比如我们的C),所以重点在数据的管理和理解。
因为,再小的程序也不太可能是一步就能完成的(即不可能一行代码),所以在程序执行的过程中就需要先把各类数据存储起来,这些数据一般都是临时存储在内存中。
那内存,又是怎么存储的呢?很简单,你可以把内存就直接的理解成为一个excel表格,只不过这个excel表格理论上是可以无限大的。
那既然是excel来存储数据就容易理解多了,我们只关注横向的列和纵向的行即可。简单来说,excel就是一行就是一个内存单元点8位(bit)也就是一个字节(byte),能表示0-255共256种数值(状态),但是每个单元格是只能存储0/1两种值的
所以,如果我们要存储一个4字节的int,在这个excel表格中就需要占4行才能存储得下。逻辑的分布可以参考如下:
内存地址 b0 b1 b2 b3 b4 b5 b6 b7
0x000001 0 0 0 0 1 0 0 0
0x000002 0 0 0 1 0 1 0 0
0x000003 0 0 1 0 0 0 0 0
0x000004 0 0 0 0 0 0 0 0
0x000005 0 0 0 0 0 0 0 0
0x000006 0 0 0 0 0 0 0 0
0x000007 0 0 0 0 0 0 0 0
0x000008 0 0 0 0 0 0 0 0
0x000009 0 0 0 0 0 0 0 0
0x000010 0 0 0 0 0 0 0 0
0x000011 0 0 0 0 0 0 0 0
0x000012 0 0 0 0 0 0 0 0
0x000013 0 0 0 0 0 0 0 0
0x000014 0 0 0 0 0 0 0 0
0x000015 0 0 0 0 0 0 0 0
0x000016 0 0 0 0 0 0 0 0
0x000017 0 0 0 0 0 0 0 0
0x000018 0 0 0 0 0 0 0 0
0x000019 0 0 0 0 0 0 0 0
0x000020 0 0 0 0 0 0 0 0
0x000021 0 0 0 0 0 0 0 0
0x000022 0 0 0 0 0 0 0 0
0x000023 0 0 0 0 0 0 0 0
0x000024 0 0 0 0 0 0 0 0
....
我们都知道计算机最终处理的都是0/1,也就是二进制,实现上为了更方便程序操作,大部分的数据处理都是以byte为单位的(如socket,文件io...),所以我们也只要重点关注byte就行了。就是一个内在单元能存储一个byte(c语言的char),有8位.
继续拿excel表格来说,我们知道每一行(内存单元)都是一个按顺序标明的行号的,在内存中这个行号就可以理解为一个内存地址,即一个指针变量存储的值。
所以,指针实际上存储的就是一个内存地址,我们所有对指针的操作都是对这个内存地址的操作。另外,不管任何数据类型对应的指针长度总是一致的(例如在64位系统,长度就是8字节),所以对指针类型的转换其实是可以任意强制转换的。比如,把一个int转换为byte是完全可以的。
内存实际上是一个非常复杂的概念,因为是直接硬件协议、驱动相关的,我们在开发的过程中只要理解基本的逻辑模型即可。即,内存的地址是连续的,单个内存单元的大小是1个字节,对指针的理解就足够了。
3.3 指针的定义和使用
定义指针的语法如下:
type * ptrVal;
定义指针的时候是需要指定指针类型的,如int 类型的指针需要定义成int* ptrInt。所以,指针变量的类型与当前程序能操作的类型是等数的,比如char,int ,float,struct....
因为,指针变量存储的就是另外一个变量的地址,所以在给指针变量赋值前必须先声明变量。然后,通过取址符&获取变量地址,通过*.如
int main(){
char b=97;
int a= 10;
int* ptrA = &a; //通过 & 获取地址
char * ptrB= NULL; //如果不确定指针值,可以先赋值为NULL
ptrB = &b;
printf("var a addr is :%p,val is :%d\n",ptrA,*ptrA); //通过%p 格式化指针地址,* 从指针中获取值
printf("var b addr is :%p,val is :%d\n",ptrB,*ptrB);
}
输出结果如下:
var a addr is :0xffffcc08,val is :10
var b addr is :0xffffcc0f,val is :97
addr 可能会不一致
好吧,对于普通变量的指针使用就是通过&和*取址和取值即可。
3.4 指针的操作
指针的操作,其实在3.3 已经做了一些说明了,比如取一个变量的地址,从指针中取值。除此之外,指针还有一个非常重的功能就是可以对指针进行运算操作。
指针的运算操作,主要有++ ,--也可以+int值或-int值。所代表的意思就是内存位置向后或向前移动(想象一下之前的 excel,在某一行的时候可以向上或者向下移动行号,其实就是在内存中移动指针了),但需要注意的是进行运算操作时移动位数=运算数*指针类型长度,比如int*指针+1,内存位置实际上是移动了4位
看代码:
int main( )
{
int m = 77;
int i = 10;
int j = 20; //定义三个变量
printf("sizeof int is :%d\n", sizeof(m));
printf("addr m:%p,addr i:%p,addr j:%p\n",&m,&i,&j); //输出三个变量地址
int * iPtr = &i;
printf("iPtr-1,addr:%p,value:%d\niPtr,addr:%p,value:%d\iPtr+1,addr:%p,value:%d\n",
(iPtr-1),*(iPtr-1), //指针减1,即向低位移指针对应变量长度的位置
iPtr,*iPtr,
(iPtr+1),*(iPtr-1)); //指针加1,即向低位移指针对应变量长度的位置
char * ptr_char = (char*)iPtr; //把int*强制转换为char*
printf("ptr_char %p\n",ptr_char);
for(int k=-4;k<8;k++){
char* tempPtr = ptr_char +k;
printf("tempPtr addr:%p,value:%d\n",tempPtr,*tempPtr);
}
/* p 是函数指针 */
}
输出结果
sizeof int is :4
addr m:0xffffcbfc,addr i:0xffffcbf8,addr j:0xffffcbf4
iPtr-1,addr:0xffffcbf4,value:20
iPtr,addr:0xffffcbf8,value:10iPtr+1,addr:0xffffcbfc,value:20
ptr_char 0xffffcbf8
tempPtr addr:0xffffcbf4,value:20
tempPtr addr:0xffffcbf5,value:0
tempPtr addr:0xffffcbf6,value:0
tempPtr addr:0xffffcbf7,value:0
tempPtr addr:0xffffcbf8,value:10
tempPtr addr:0xffffcbf9,value:0
tempPtr addr:0xffffcbfa,value:0
tempPtr addr:0xffffcbfb,value:0
tempPtr addr:0xffffcbfc,value:77
tempPtr addr:0xffffcbfd,value:0
tempPtr addr:0xffffcbfe,value:0
tempPtr addr:0xffffcbff,value:0
不同电脑结果可能不同,addr也必然不同
通过示例相信就会有对指针操作有一个更深刻的理解。另外,这仅仅只是作为演示使用,使用指针访问其它变量的值其实是一个非常非常非常危险的操作。会让结果变得不可控,甚至crash。这里仅仅只是演示使用,实际过程中千万别模仿。
3.4 数组指针
数组指针,即指向一个数组的指针。事实上,该数组指针只是指向了指针的第一个元素。
看代码
int main( )
{
int MAX = 4;
int intarr[] = {77,88,99,111};
int* arrPtr = & intarr[0];
for(int i=0;i<MAX;i++){
printf("addr is:%p,value is:%d\n",(arrPtr+i),*(arrPtr+i));
}
}
运行结果:
addr is:0xffffcc00,value is:77
addr is:0xffffcc04,value is:88
addr is:0xffffcc08,value is:99
addr is:0xffffcc0c,value is:111
相信理解了指针,数组指针也是非常好理解的。
3.5 结构指针
即指针一个结构的指针,由于C语言的结构内存是顺序排列的,所以结构体指针实际上也是结构的第一个元素的指针
int main( )
{
PERSON p;
p.age = 40;
strcpy(p.name,"张三");
p.addr = (char*)"地址是这个喽";
PERSON* ptrP = &p; //同样的方法获取指针
printf("ptrP addr:%p,first member addr:%p\n",ptrP,&(p.age));
printf("struct var access,age:%d,name:%s,addr:%s\n",p.age,p.name,p.addr);
printf("ptr access,age:%d,name:%s,addr:%s\n",ptrP->age,ptrP->name,ptrP->addr); //结构指针使用->访问成员
PERSON *firstPtr = (PERSON*)&p.age;
printf("firstPtr access,age:%d,name:%s,addr:%s\n",firstPtr->age,firstPtr->name,firstPtr->addr);
}
运行结果如下:
ptrP addr:0xffffcbe0,first member addr:0xffffcbe0
struct var access,age:40,name:张三,addr:地址是这个喽
ptr access,age:40,name:张三,addr:地址是这个喽
firstPtr access,age:40,name:张三,addr:地址是这个喽
ptr2 addr:0xffffcbb0,p2 access,age:40,name:张三,addr:地址是这个喽
说明指针类型本身是可以任意转换的,因为只是代表一个行号
3.6 函数指针
函数指针简单来理解就是,指针函数的一个指针。有了函数指针,我们可以把一个函数作为参数传递给方法然后进行回调。甚至,可以在结构中定义函数指针实现类似类的封装效果。
关于函数回调,其实不同语言的实现方式差别很大的。如Java8之前只能通过定义接口的方法传递;C#则一开始以委托、事件的方式进行传递。其它的动态类型语言,如PHP,Python,nodejs则使用使用匿名函数、闭包、lambda表达式等进行传递。
函数指针使用一般分X步。
- 定义一个函数
- 把函数指针赋值给一个指针变量
- 通过函数指针的方式调用函数
具体看代码
#include <stdio.h>
int add(int x,int y){
return x+y;
}
int minus(int x,int y){
return x-y;
}
int calc(int x,int y,int (*calcFuns)(int x,int y)){
//作为一个回调使用
printf("calcFuns addr:%p\n",calcFuns);
return calcFuns(x,y);
}
int main( )
{
printf("calc(3,4,add) %d\n",calc(3,4,add));
printf("calc(3,4,minus) %d\n",calc(3,4,minus));
int (*myFuncs)(int, int) = &add; //&是可以选的
printf("add addr:%p,myFuncs addr:%p,call my funcs:%d\n",add, myFuncs, myFuncs(3,4));
return 0;
}
calcFuns addr:0x100401080
calc(3,4,add) 7
calcFuns addr:0x100401094
calc(3,4,minus) -1
add addr:0x100401080,myFuncs addr:0x100401080,call my funcs:7
通过结果可以发现,其实我们在使用函数名调用函数的时候其实就是用到了函数地址。很有意思吧。
4. 动态内存管理
动态内存分配简单来讲就是可以让我们随心所欲的分配内存、管理内存以及释放内存,从性能和内存利用效率上来说都是非常高的。
但问题也来了,那就是需要自己适时释放内存,否则手动申请的内存不管是在方法内还是方法外都无法释放的。
主要是malloc,calloc,free几个函数的使用。
动态分配内存的时候,如果没有释放则会一直存在,所以使用的时候一定要小心。
野指针是指,指针的被释放了,但指针变量还存在,所以在释放指针的时候,记得设置成NULL。
4.1 使用malloc分配内存
废话少说看代码
#include <stdio.h>
#include <stdlib.h> //malloc在该头文件中
#include <memory.h> //memset在该头文件中
int main(){
int MAX = 5; //定义数组大小
int* intPtr = (int*)malloc(sizeof(int) * 5); //使用malloc分配内存
// int* intPtr = (int*)malloc(sizeof(int) * 1024*1024*1024*1024); 尝试申请4T内存?
if(intPtr == NULL){ //内存是向操作系统申请的,操作系统完全可能因为系统内存不足,或权限设置拒绝程序的内存申请
printf("内存申请失败...");
exit(-1);
}
memset(intPtr,1, sizeof(int)*5); //一般malloc申请内存不进行分配(具体也看编译平台),使用memset给每个块设置一个值即 1<<24|1<<16|1<<8|1
for (int i = 0; i < MAX; ++i) {
printf("addr %p,value:%d\n",(intPtr+i),*(intPtr+i)); //查看一下原有内存地址
*(intPtr+i) = i+1; //给内存地址赋值
}
printf("-------------\n");//赋值以后输出
for (int i = 0; i < MAX; ++i) {
printf("addr %p,value:%d\n",(intPtr+i),*(intPtr+i));
}
free(intPtr); //不需要变量,一定要记得free
// *intPtr = 34;
// printf("lala...%d\n",*intPtr);
//可以尝试一下使用注销下代码,看会发生什么...
//想想为什么?
intPtr =NULL; //free后,intPtr成野指针了。该内存地址就不可用了,把指针归零.
return 0;
}
执行结果
addr 0x600000350,value:16843009
addr 0x600000354,value:16843009
addr 0x600000358,value:16843009
addr 0x60000035c,value:16843009
addr 0x600000360,value:16843009
-------------
addr 0x600000350,value:1
addr 0x600000354,value:2
addr 0x600000358,value:3
addr 0x60000035c,value:4
addr 0x600000360,value:5
16843009 = 1<<24|1<<16|1<<8|1
4.2 使用calloc分配内存
calloc是在memeory.h头文件中,作用也是分配内存。但该内存分配需要指定长度和单个大小,同时使用calloc分配内存会自动始化。
所以,上面的代码很容易改成,calloc分配
int MAX = 5; //定义数组大小
int* intPtr = (int*)calloc(5,sizeof(int) ); //使用calloc分配内存
....
//memset(intPtr,1, sizeof(int)*5); //这行不需要了,因为calloc会始化为零
...
执行结果,可以自行运行,其实还是有差别的.
4.3 free的一些补充
通过4.1的例子我们已经知道free是怎么用的了,但是当你注释掉如下两行:
// *intPtr = 34;
// printf("lala...%d\n",*intPtr);
你会神奇的发现代码能按照我们预期运行(就这个例子而言),等等难道我使用了假的free?这free竟然没有效果?怎么回事?
其实少侠别慌,该例子已经在前面说过了,这样做是秀危险的。拿个很简单的例子说吧,比如你租了房和房东说好退租了,东西也搬走了,押金也退回了。但是你之前房子地址、钥匙什么的都有。所以你如果偷偷的跑回去,刚好房子又没人,那老实说你再放点东西,住个几天,其实从某些程序上来说也许相安无事。
但是,但是。。。你要知道,你已经退租了,那房东肯定会把房子再租出去。可以你再去的时候别人已经住下了,或者你住的时候,别人又住了(把你的东西丢了)。所以,这种情况是存在各种不确定性的,是很危险的。
所以想想危险性,还是把钥匙也还得房东,地址也忘记吧(inpPtr=NULL).
5. 总结
当然C语言还存在很多细节点,但本节介绍的预处理器、结构体、指针可以说是平时用得最多的了。这三样东西理解透了,一般的C代码要看就不难了。
参数资料:
- C 语言教程 | 菜鸟教程 绝对够菜鸟,也够全面,推荐花半天时间看一遍
- C语言预处理命令详解全面介绍预处理指针
- C程序的内存布局(Memory Layout) 全面理解内存的补充资料