C语言基础二-指针,预处理命令

-----我们知道,数组中的所有元素在内存中是连续排列的,如果一个指针指向了数组中的某个元素,那么加 1 就表示指向下一个元素,减 1 就表示指向上一个元素,这样指针的加减运算就具有了现实的意义,这个有意义的应用我们后面在具体写demo来研究。

------不过C语言并没有规定变量的存储方式,如果连续定义多个变量,它们有可能是挨着的,也有可能是分散的,这取决于变量的类型、编译器的实现以及具体的编译模式,所以对于指向普通变量的指针,做加减运算没有意义,因为不知道它后面指向的是什么数据。

------指针变量除了可以参与加减运算,还可以参与比较运算。当对指针变量进行比较运算时,比较的是指针变量本身的值,也就是数据的地址。如果地址相等,那么两个指针就指向同一份数据,否则就指向不同的数据。

-----另外需要说明的是,不能对指针变量进行乘法、除法、取余等其他运算,除了会发生语法错误,也没有实际的含义。

4、数组指针

数组(Array)是一系列具有相同类型的数据的集合,每一份数据叫做一个数组元素(Element)。数组中的所有元素在内存中是连续排列的,整个数组占用的是一块内存。以int arr[] = { 99, 15, 100, 888, 252 };为例,该数组在内存中的分布如下图所示:

1J35014B-0.jpeg

定义数组时,要给出数组名和数组长度,数组变量名可以被认为就是一个指针,它指向数组的第 0 个元素。在C语言中,我们将第 0 个元素的地址称为数组的首地址。以上面的数组为例,下图是 arr 的指向:

2.jpeg

结合我们前面分析的指针变量的加减运算,循环遍历数组元素,我们还可以这样干:

#include <stdio.h>
int main(){
    int arr[] = { 99, 15, 100, 888, 252 };
    int len = sizeof(arr) / sizeof(int);  //求数组长度
    int i;
    for(i=0; i<len; i++){
        printf("%d  ", *(arr+i) );  //*(arr+i)等价于arr[i]
    }
    printf("\n");
    return 0;
}

我们也可以定义一个指向数组的指针变量,例如:

int arr[] = { 99, 15, 100, 888, 252 };
int *p = arr;     //数组类型变量,就不建议在画蛇添足,前面在加一个&

  arr 本身可以被认为是一个指针,可以直接赋值给指针变量 p:int *p = arr;
  arr 是数组第 0 个元素的地址,所以int *p = arr;也可以写作int *p = &arr[0];。
也就是说,arr、p、&arr[0] 这三种写法都是等价的,它们都指向数组第 0 个元素,或者说指向数组的开头的内存地址。
    如果一个指针变量指向了数组,我们就称它为数组指针变量(Array Pointer)。
    数组指针指向的是数组中的一个具体元素,而不是整个数组,所以数组指针的类型和数组元素的类型有关,上面的例子中,p 指向的数组元素是 int 类型,所以 p 的类型必须也是int *。
    反过来想,p 并不知道它指向的是一个数组,p 只知道它指向的是一个整数,究竟如何使用 p 取决于程序员的编码。
更改上面的代码,使用数组指针来遍历数组元素:

#include <stdio.h>
int main(){
    int arr[] = { 99, 15, 100, 888, 252 };
    int *p = arr
  //int *p = (int *)&arr;   //画蛇添足,没必有要这样写代码
  //int *p = &arr[2];    //arr[2]是普通变量了,这时要加&去取它的地址
    int len = sizeof(arr) / sizeof(int);
    for(int i=0; i<len; i++){
        printf("%d  ", *(p+i) );   //99  15  100  888  252
        printf("%d  ",p[i]);
    }    
    printf("\n");
    return 0;
}
//执行结果:
99  99  15  15  100  100  888  888  252  252  


//更改上面的代码,让 p 指向数组中的第二个元素:
#include <stdio.h>
int main(){
    int arr[] = { 99, 15, 100, 888, 252 };
    int *p = &arr[2];  //也可以写作 int *p = arr + 2;
    printf("%d, %d, %d, %d, %d\n", *(p-2), *(p-1), *p, *(p+1), *(p+2) );
    return 0;
}

注意:不能使用指针【如:sizeof(p) / sizeof(int)】计算数组长度.
总结访问数组元素的方法:

  1. 使用下标
    arr[i] 的形式访问数组元素。如果 p 是指向数组 arr 的指针,也可以使用 p[i] 来访问数组元素。

  2. 使用指针
    使用 *(p+i) 的形式访问数组元素。另外数组名本身也可以认为是指针,使用 *(arr+i) 来访问数组元素。
    注意:arr当成指针来用的时候,是个常量,不能修改,始终指向数组首地址。

关于数组指针的谜题
       假设 p 是指向数组 arr 中第 n 个元素的指针,那么 *p++、*++p、(*p)++ 分别是什么意思呢?
    *p++ 等价于两步 p; p++;,表示先取得第 n 个元素的值,再将 p 指向下一个元素。
    
++p 等价于 *(++p),会先进行 ++p 运算,使得 p 的值增加,指向下一个元素,整体上相当于 (p+1),所以会获得第 n+1 个数组元素的值。
    (
p)++ 就非常简单了,会先取得第 n 个元素的值,再对该元素的值加 1。假设 p 指向第 0 个元素,并且第 0 个元素的值为 99,执行完该语句后,第 0 个元素的值就会变为 100。

5、字符串指针

C语言中没有专门的字符串类型,前面我们讲了可以用字符数组表示字符串,除此之外,
C语言还支持另外一种表示字符串的方法,就是直接使用一个指针指向字符串,例如:

char *str = "helloworld!";

等价于:

char *str;  //定义指针变量
str = "helloworld!";  //把字符串的首地址赋给str变量。内部把这个字符串理解为一个数组也可以。

直接输出字符串

  printf("%s\n", str);
    //%s表示输出一个字符串,给出字符指针变量名str,则系统先输出它所指向的一个字符数据,
    //然后自动使string加1,使之指向下一个字符,然后再输出一个字符,……,如此直到遇到字符串结束标志‘\0’为止。

和整数指针变量,不同哈,要区别,把字符串指针看成一个单独的特殊的类型!
int *i = 10; 不能等价于: int *i; i=10;
输出内容:sprintf("%d",i);不对的哈!
通过字符数组名或字符指针变量可以输出一个字符串。
而对一个数值型数组,是不能企图用数组名输出它的全部元素的。
字符串是特殊的!可以把字符串看作为一个整体来处理,可以对一个字符串进行整体的输入输出。
还可以用指针和数组的方式输出字符串:

#include <stdio.h>
#include <string.h>
int main(){
    char *str = "helloworld!";
    int len = strlen(str), i;

    //使用*(str+i)
    for(i=0; i<len; i++){
        printf("%c", *(str+i));
    }
    printf("\n");
    
    //使用str[i]
    for(i=0; i<len; i++){
        printf("%c", str[i]);
    }
    printf("\n");
    return 0;
}

    看起来和字符数组是差不多,它们都可以使用%s输出整个字符串,都可以使用*或[ ]获取单个字符,这两种表示字符串的方式是不是就没有区别了呢?
   有! 它们最根本的区别是在内存中的存储区域不一样,字符数组存储在可读写数据段(全局数据区)或栈区,指针形式的字符串存储在只读数据段(常量区)。全局数据区和栈区的字符串有读取和写入的权限,而常量区的字符串只有读取权限,没有写入权限。
    内存权限的不同导致的一个明显结果就是,字符数组在定义后可以读取和修改每个字符,而对于指针形式的字符串,一旦被定义后就只能读取不能修改,任何对它的赋值都是错误的。
    我们将指针形式的字符串称为字符串常量,意思很明显,常量只能读取不能写入。

#include <stdio.h>
int main(){
    char *str = "Hello World!";
    str[3] = 'P';  //错误
    return 0;
}
#include <stdio.h>
int main(){
    char *str = "Hello World!";
    str = "abc123";
   // *str = "abc123";                   //有星号和没有星号的区别!
   int len = sizeof(str)/sizeof(char);   //数组长度,指针所指的字符串当数组了
   int i;
   printf("%s\n",str);
   for(i=0;i<len;i++){
    printf("%c",str[i]);
   }
   printf("\n");
    return 0;
}

image.png

linux上
image.png

    到底使用字符数组还是字符串常量
  在编程过程中如果只涉及到对字符串的读取,那么字符数组和字符串常量都能够满足要求;
  如果有写入(修改)操作,那么只能使用字符数组,不能使用字符串常量。
典型的选择字符数组的场景:获取用户输入的字符串,只能使用字符数组:

#include <stdio.h>
int main(){
    char str[30];
    gets(str);
    printf("%s\n", str);
    return 0;
}

总结:C语言有两种表示字符串的方法,一种是字符数组,另一种是字符串常量,它们在内存中的存储位置不同,使得字符数组可以读取和修改,而字符串常量只能读取不能修改。

指针变量作为函数参数

    上一章,我们讲解了函数,但是参数传递中,我们演示的Demo都是用的值传递,现在学习了指针后,我们可以对函数参数传递做一个总结,函数参数传递分值传递和地址传递:
首先:我们再列一个值传递的例子

#include <stdio.h>
void swap(int x, int y){
    int temp;  //临时变量
    temp = x;
    x = y;
    y = temp;
}
int main(){
    int a = 66, b = 99;
    swap(a, b);
    printf("a = %d, b = %d\n", a, b);
    return 0;
}
//执行结果: a,b没有交换值
a = 66, b = 99

    值传递是相当于复制一份数据,并没有改变数据值。
我们再来地址传递

#include <stdio.h>
void swap(int *p1, int *p2){
    int temp;  //临时变量
    temp = *p1;
    *p1 = *p2;
    *p2 = temp;
}
int main(){
    int a = 66, b = 99;
    swap(&a, &b);
    printf("a = %d, b = %d\n", a, b);
    return 0;
}
//执行结果
a = 99, b = 66

    对数组这样的大内存块,进行参数传递的时候必须用指针,计算机底层是不允许大内存块直接进行拷贝数据的,那样如果数组达到亿级时,程序卡死的怀颖人生:

#include <stdio.h>
int max(int *intArr, int len){   //数组长度要由调用者提供,值传递是拿不到的
    int i, maxValue = intArr[0];  //假设第0个元素是最大值
    for(i=1; i<len; i++){
        if(maxValue < intArr[i]){
            maxValue = intArr[i];
        }
    }
   
    return maxValue;
}
int main(){
    int nums[6]={22,11,55,88,99,66,33};
    int len = sizeof(nums)/sizeof(int);
    printf("Max value is %d!\n",max(nums,len));
    return 0;
}

指针作为函数返回值

C语言允许函数的返回值是一个指针(地址),我们将这样的函数称为指针函数。
下面的例子定义了一个函数 longstr(),用来返回两个字符串中较长的一个:

#include <stdio.h>
//两个字符串,返回一个指针,指向字符串最长的那个
char * longstr(char * str1,char * str2){  //c语言中没有字符串String这种数据,只能用char数组或指针表示
   if(strlen(str1)>= strlen(str2)){
    return str1;
   }else{
    return str2;
   }

}
int main(){
    char a[30]={0},b[30]={0};
    gets(a);   //linux中,会有警告,提示gets是危险的,不建议使用,用了也不会错
    gets(b);
    char * rsstr = longstr(a,b);
    printf("较长的字符为: %s\n",rsstr);
    return 0;
}

用指针作为函数返回值时需要注意的一点是,函数运行结束后我们认为会销毁在它内部定义的所有局部数据,包括局部变量、局部数组和形式参数,函数返回的指针请尽量不要指向这些数据,C语言没有任何机制来保证这些数据会一直有效,它们在后续使用过程中可能会引发运行时错误,所以C编译器是不会通过编译的。
如下代码是不好的代码:

#include <stdio.h>
int *func(){
    int n = 100;
    return &n;     //程序编译之前就会发出警告信息,提示将会发生不可知的错误。
}

int main(){
    int *p = func(), n;
    n = *p;
    printf("value = %d\n", n);
    return 0;
}

指向指针的指针,空指针NULL以及void指针

指针可以指向一份普通类型的数据,例如 int、double、char 等,也可以指向一份指针类型的数据,例如 int *、double *、char * 等。

如果一个指针指向的是另外一个指针,我们就称它为二级指针,或者指向指针的指针。

假设有一个 int 类型的变量 a,p1是指向 a 的指针变量,p2 又是指向 p1 的指针变量,它们的关系如下图所示:


544314910-0.jpeg

将这种关系转换为C语言代码:

int a =100;
int *p1 = &a;
int **p2 = &p1;

指针变量也是一种变量,也会占用存储空间,也可以使用&获取它的地址。
C语言不限制指针的级数,每增加一级指针,在定义指针变量时就得增加一个星号
p1 是一级指针,指向普通类型的数据,定义时有一个

p2 是二级指针,指向一级指针 p1,定义时有两个*。

如果我们希望再定义一个三级指针 p3,让它指向 p2,那么可以这样写:

int ***p3 = &p2;

四级指针也是类似的道理:

int ****p4 = &p3;

实际开发中会经常使用一级指针和二级指针,几乎用不到高级指针。
    想要获取指针指向的数据时,一级指针加一个,二级指针加两个,三级指针加三个*,以此类推,请看代码:

#include <stdio.h>
int main(){
    int a =100;
    int *p1 = &a;
    int **p2 = &p1;
    int ***p3 = &p2;
    printf("%d, %d, %d, %d\n", a, *p1, **p2, ***p3);
    printf("&p2 = %#X, p3 = %#X\n", &p2, p3);
    printf("&p1 = %#X, p2 = %#X, *p3 = %#X\n", &p1, p2, *p3);
    printf(" &a = %#X, p1 = %#X, *p2 = %#X, **p3 = %#X\n", &a, p1, *p2, **p3);
    return 0;
}
//执行结果
100, 100, 100, 100
&p2 = 0X61FE00, p3 = 0X61FE00
&p1 = 0X61FE08, p2 = 0X61FE08, *p3 = 0X61FE08
 &a = 0X61FE14, p1 = 0X61FE14, *p2 = 0X61FE14, **p3 = 0X61FE14

1)空指针 NULL
一个指针变量可以指向计算机中的任何一块内存,不管该内存有没有被分配,也不管该内存有没有使用权限,只要把地址给它,它就可以指向,C语言没有一种机制来保证指向的内存的正确性,程序员必须自己提高警惕。

很多初学者会在无意间对没有初始化的指针进行操作,这是非常危险的,请看下面的例子:

#include <stdio.h>
int main(){
    char *str; //声明了,没有初始化,局部变量,编译器会随机给一个垃圾值!
    gets(str);  //程序直接在这里就崩溃了!
    printf("%s\n", str);
    return 0;
}

不要直接使用未初始化的局部变量。
上面的代码中,str 就是一个未初始化的局部变量,它的值是不确定的,究竟指向哪块内存也是未知的,大多数情况下这块内存没有被分配或者没有读写权限,使用 gets() 函数向它里面写入数据当然会出错的。

#include <stdio.h>
int main(){
    char as[30];
    char *str = as; //声明了,没有初始化,局部变量,编译器会随机给一个垃圾值!
    gets(str);  //程序直接在这里就崩溃了!
    printf("您输入的字符:%s\n", str);
    return 0;
}

程序很复杂了后,遇到不确定初始值的指针变量的时候一般情况下我们会赋值为NULL指针:
char *str=NULL;
    NULL 是“零值、等于零”的意思,在C语言中表示空指针。
注意区分大小写,null 没有任何特殊含义。

#include <stdio.h>
int main(){
    char *str = NULL;   //这里不定义空指针的话,会卡在这里
    gets(str);           //系统不作处理,即不会让我们输入数据
    printf("%s\n", str);   
    return 0;
}

程序不会阻塞,我们有理由怀疑gets()函数,对传入的变量,进行了判断如果是NULL,就什么也不做,当然就不会阻塞程序等用户输入了!

我们在自己定义的函数中也可以进行类似的判断,例如:

void func(char *p){
    if(p == NULL){
        printf("(null)\n");
    }else{
        printf("%s\n", p);
    }
}

这样能够从很大程度上增加程序的健壮性,防止程序意外崩溃或出错。
2)NULL到底是什么?void指针
    NULL 是在stdio.h中定义的一个宏,它的具体内容为:
#define NULL ((void *)0)
    (void *)0表示把数值 0 强制转换为void *类型,最外层的( )把宏定义的内容括起来,防止发生歧义。
从整体上来看,NULL 指向了地址为 0 的内存。
    最低地址处有一段内存区域被称为保留区,这个区域不存储有效数据,也不能被用户程序访问,将 NULL 指向这块区域很容易检测到违规指针。

注意,C语言没有规定 NULL 的指向,只是大部分标准库约定成俗地将 NULL 指向 0,所以不要将 NULL 和 0 等同起来,例如下面的写法是不专业的:
int *p = 0;
而应该坚持写为:
int *p = NULL;

    注意 NULL 和 NUL 的区别:NULL 表示空指针,是一个宏定义,可以在代码中直接使用。而 NUL 表示字符串的结束标志 '\0',它是ASCII码表中的第 0 个字符。NUL 没有在C语言中定义,仅仅是对 '\0' 的称呼,不能在代码中直接使用。
void用在函数定义中可以表示函数没有返回值或者没有形式参数,

用在指针里,表示指向的数据的类型是未知的。
也就是说,void *表示一个有效指针,它确实指向实实在在的数据,只是数据的类型尚未确定,在后续使用过程中一般要进行强制类型转换。

#include <stdio.h>
#include <string.h>
void *strlong(void *str1, char *str2){
    if(strlen(str1) >= strlen(str2)){
        return str1;
    }else{
        return str2;
    }
}
int main(){
    char str1[30], str2[30], *str;
    gets(str1);
    gets(str2);
    str = (char *)strlong(str1, str2);
    printf("Longer string: %s\n", str);
    return 0;
}

总结:void *是一种指针类型,我们可以将别的类型的指针无需强制类型转换的赋值给void *类型。也可以将void *强制类型转换成任何别的指针类型,至于强转的类型是否合理,就需要我们程序员自己控制了。

指针数组

-----如果一个数组中的所有元素保存的都是指针,那么我们就称它为指针数组。

指针数组的定义形式一般为:

dataType * arrayName[length];

----除了每个元素的数据类型是指针以外,指针数组和普通数组在其他方面都是一样的,下面是一个简单的例子:

#include <stdio.h>
int main(){
    int a = 16, b = 932, c = 100;
    //定义一个指针数组
    int *arr[3] = {&a, &b, &c};//也可以不指定长度,直接写作 int *arr[]

    for(int i=0;i<3;i++){
        printf("%d\n",*arr[i]);
    }
    return 0;
}

指针数组还可以和字符串数组结合使用,请看下面的例子:

#include <stdio.h>
int main(){
    char *str[3] = {
        "hello",
        "world",
        "c language"
    };
    printf("%s\n%s\n%s\n", str[0], str[1], str[2]);
    return 0;
}

字符数组 str 中存放的是字符串的首地址,不是字符串本身,字符串本身位于其他的内存区域,和字符数组是分开的,等价写法:

#include <stdio.h>
int main(){
    char *str0 = "hello";
    char *str1 =  "world";
    char *str2 = "c language";
    char *str[3] = {str0, str1, str2};
    printf("%s\n%s\n%s\n", str[0], str[1], str[2]);
    return 0;
}

练习

#include <stdio.h>
int main(){
    char * lines[6] = {
        "xiongshaowen",
        "xuhuifeng",
        "熊少文",
        "徐会凤",
        "isxuhuifengxiongshaowen",
        "fuqi"
    };
    char * str1 = lines[1];
    char * str2 = *(lines +3);  //先让lines移动3个位置,再用*取出字符串值,再赋给str2地址指向它
    char c1 = *(*(lines+4)+6);   //‘isxuhuifengxiongshaowen"的第七个字符
    char c2 =(*lines + 5)[5];    //'xiongshaowen'的首地址 移5位到,再取它所指地址后的当一个数组的第六个元素即e
    char c3 = *lines[0] +2;      // 'x'+2,即x的ASSIC值加2,变为z
    char c4 = *(lines+1)[0]+2;   //第二个指针指向的字符串的第一个元素x+2,即也为z

    printf("str1= %s\n",str1);   //xuhuifeng
    printf("str2= %s\n",str2);   //徐会凤
    printf(" c1= %c\n",c1);     //i
     printf(" c2= %c\n",c2);    //e
   printf(" c3= %c\n",c3);      //z
    printf(" c4= %c\n",c4);     //z

}

二维数组指针(指向二维数组的指针)

    二维数组在概念上是二维的,有行和列,但在内存中所有的数组元素都是连续排列的,它们之间没有“缝隙”。

以下面的二维数组 a 为例:

int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };

从概念上理解,a 的分布像一个矩阵:

0 1 2 3 4 5 6 7 8 9 10 11

但在内存中,a 的分布是一维线性的,整个数组占用一块连续的内存:

1.png

    另外,逻辑上二维数组是二维的,但是本质上C语言允许把一个二维数组分解成多个一维数组来处理。

int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };
2.png

总结:数组指针,指针数组,二维数组指针!

数组指针:指针变量指向一维数组中某个元素,这个指针变量就是数组指针。

int a[]={1,2,3};
 int *pa = a;

指针数组:数组里的元素都是指针叫指针数组。

dataType * arrayName[length];

二维数组指针:指针变量指向二维数组中的某个元素。

int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };
int (*p)[4] = a;

    括号中的表明 p 是一个指针,它指向一个数组,数组的类型为int [4],这正是 a 所包含的每个一维数组的类型。
    [ ]的优先级高于
,( )是必须要加的,如果赤裸裸地写作int * p[4],p 就成了一个指针数组,而不是二维数组指针。

    二维数组指针进行加法(减法)运算时,它前进(后退)的步长与它指向的数据类型有关,p 指向的数据类型是int [4],那么p+/-1就移动 4×4 = 16 个字节,在二维数组来看,就是指针加减1,指针就移动一行。
    二维数组用下标获取元素前面讲过了,这里我们讨论如何使用指针 p 来访问二维数组中的每个元素

  1. p指向数组 a 的开头,也即第 1 行,下标为0;p+1前进一行,指向第 2 行,下标为1。
  2. *(p+1)表示取地址上的数据,也就是整个第 2 行数据。注意是一行数据,是多个数据,不是第 2 行中的第 1 个元素,下面的运行结果有力地证明了这一点:
#include <stdio.h>
int main(){
    int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };
    int (*p)[4] = a;   //定义二维指针数组
    printf("%d\n", sizeof(*(p+1)));  //16,表示这个数据有16个字节,4个int数据
  //用数组指针遍历二维数组
    int i=0,j=0;
    for(i=0;i<3;i++){
        for(j=0;j<4;j++){
            printf("%-4d",*(*(p+i)+j));
        }
        printf("\n");
    }
    return 0;
}
//执行结果
16
0   1   2   3   
4   5   6   7   
8   9   10  11  

这里有问题需要讨论一下:
  数组变量名,二维数组指针加减法运算后的结果,有时候表示的是一个指针(就是一个内存地址),
   有的时候表示的是数组本身,一个整体的数组,你们自己要灵活的理解,正确的理解,
   当我们数组变量名和上面的二维数组指针在定义的时候,在做sizeof函数的参数的时候,代表的是整体的数组本身,
   当我们把数组变量名和上面的二维数组指针放在表达式里的时候,代表是就是一个具体的指针(就是一个内存地址)

  1. *(p+1)+1表示第2 行第2 个元素的地址, ((p+1)+1)表示第 2 行第 2 个元素的值。如何理解呢?
#include <stdio.h>
int main(){
   
    int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };
    int (*p)[4] = a;
    printf("%d",*(*(p+1)+1));

    return 0;
}

总结:根据上面的结论,使用指针遍历二维数组。

#include <stdio.h>
int main(){
    int a[3][4]={0,1,2,3,4,5,6,7,8,9,10,11};
    int(*p)[4];
    int i,j;
    p=a;
    for(i=0; i<3; i++){
        for(j=0; j<4; j++){
            printf("%-4d  ",*(*(p+i)+j));
        }
        printf("\n");
    }
    return 0;
}

函数指针(指向函数的指针)

   一个函数总是占用一段连续的内存区域,函数名在表达式中有时也会被转换为该函数所在内存区域的首地址,这和数组名非常类似。
   我们可以把函数的这个首地址(或称入口地址)赋予一个指针变量,使指针变量指向函数所在的内存区域,然后通过指针变量就可以找到并调用该函数。这种指针就是函数指针。

函数指针的定义形式为:

returnType (*pointerName)(参数列表);

   第一个括号不能省略,如果写作returnType *pointerName(参数列表);就成了函数原型,它表明函数的返回值类型为returnType *。

#include <stdio.h>
//返回两个数中较大的一个
int max(int a, int b){
    return a>b ? a : b;
}
int main(){
    //定义函数指针
    int (*pmax)(int, int) = max;  //也可以写作int (*pmax)(int a, int b)
     int maxval = (*pmax)(11, 12);  //调用语法指针变量前面加星号。
    printf("Max value: %d\n", maxval);
    return 0;
}

   函数指针一般的时候用不到,它的好处有提供调用的灵活性,提供封装性,避免命名冲突!

main()函数高级用法

   main() 是C语言程序的入口函数,有且只能有一个,它实际上有两种标准的原型:
int main();
int main(int argc, char *argv[]);
   前面的课程中我们一直使用第一种原型,它简单易懂,能让初学者很快入手。
第二种原型在实际开发中也经常使用,它能够让我们在程序启动时给程序传递数据。

#include <stdio.h>
int main(int argc, char *argv[]){
    int i;
    printf("The program receives %d parameters:\n", argc);
    for(i=0; i<argc; i++){
        printf("%s\n", argv[i]);
    }
    return 0;
}

指针的总结

image.png

怎么样对付这些复杂指针:
C语言标准规定,对于一个符号的定义,编译器总是从它的名字开始读取,然后按照优先级顺序依次解析。注意,从名字开始,不是从开头也不是从末尾,这是理解复杂指针的关键!

对于初学者,有几种运算符的优先级非常容易混淆,它们的优先级从高到低依次是:
1)定义中被括号( )括起来的那部分。
2)后缀操作符:括号( )表示这是一个函数,方括号[ ]表示这是一个数组。
3)前缀操作符:星号*表示“指向xxx的指针”。

练习理解下面的含有指针的表达式:

  1. int * p1[6];
    从 p1 开始理解,它的左边是 *,右边是 [ ],[ ] 的优先级高于 *,所以编译器先解析p1[6],p1 首先是一个拥有 6 个元素的数组,然后再解析int *,它用来说明数组元素的类型。它指针数组。

  2. int (p3)[6];
    从 p3 开始理解,( ) 的优先级最高,编译器先解析(
    p3),p3 首先是一个指针,剩下的int [6]是 p3 指向的数据的类型,它是二维数组指针。

  3. int (p4)(int a, int b);
    从 p4 开始理解,( ) 的优先级最高,编译器先解析(
    p4),p4 首先是一个指针,它后边的 ( ) 说明 p4 指向的是一个函数,括号中的int, int是参数列表,开头的int用来说明函数的返回值类型。整体来看,p4 是一个指向原型为int func(int, int);的函数的指针,它是函数指针,

  4. char ( c[10])(int **p);
    这个定义有两个名字,分别是 c 和 p,首先确认那个是主变量名,一般两个括号,后面的括号是函数后缀,里边是函数的参数。

应该是以 c 作为主变量的名字,先来看括号内部(绿色粗体):
char * (* c[10]) (int **p);
[ ] 的优先级高于 ,编译器先解析c[10],c 首先是一个数组,它前面的表明每个数组元素类型都是一个指针,说明 c 是一个指针数组,只是指针指向的数据类型尚未确定。

把指针数组抠出来,看剩下的部分:
char * (* c[10]) (int **p);
绿色粗体表明 c 是一个指针数组,红色粗体表明指针指向的数据类型,合起来就是:c 是一个拥有 10 个元素的指针数组,每个指针指向一个原型为char *func(int **p);的函数。

  1. int (((pfunc)(int ))[5])(int );
    从 pfunc 开始理解,先看括号内部(绿色粗体):
    int (
    (
    (
    pfunc)(int *))[5])(int *);
    pfunc 是一个指针。

跳出括号,看它的两边(红色粗体):
int (((*pfunc)(int *))[5])(int *);
根据优先级规则应该先看右边的(int *),它表明这是一个函数,int 是参数列表。再看左边的,它表明函数的返回值是一个指针,只是指针指向的数据类型尚未确定。

把上面已经理解的整个部分扣掉,看剩下的部分就应该是说明函数返回什么类型的指针。
int (* ((pfunc)(int *)) [5])(int *);

我们接着分析,再向外跳一层括号(红色粗体):
int (* ((pfunc)(int *)) [5])(int );
[ ] 的优先级高于 ,先看右边,[5] 表示这是一个数组,再看左边, 表示数组的每个元素都是指针。也就是说,
[5] 是一个指针数组,函数返回的指针就指向这样一个数组。

那么,指针数组中的指针又指向什么类型的数据呢?再向外跳一层括号(橘黄色粗体):
int (* ((pfunc)(int *)) [5]) (int *);
先看橘黄色部分的右边,它是一个函数,再看左边,它是函数的返回值类型。也就是说,指针数组中的指针指向原型为int func(int *);的函数。
将上面的三部分合起来就是:pfunc 是一个函数指针(蓝色部分),该函数的返回值是一个指针,它指向一个指针数组(红色部分),指针数组中的指针指向原型为int func(int *);的函数(橘黄色部分)。

预处理命令和头文件编写

预处理命令

1、预处理命令和预处理的意义
    前面各章中,已经多次使用过#include命令。
使用库函数之前,应该用#include引入对应的头文件。
这种以#号开头的命令称为预处理命令。

    回顾用编译器gcc,从一个C语言的源文件到执行,要经历的过程:
1、编写C源文件
2、预处理 gcc -E hello.c > hello.i
3、编译 gcc -S hello.i
4、汇编 gcc -c hello.s
5、链接 gcc hello.o -o hello
6、可执行文件 hello 就可以执行当前目标下的hello了。

    预处理就是在编译之前还需要对源文件进行简单的处理,为编译做准备,这种准备有时候是非常有必要,有意义的:
例如,我们希望自己的程序在 Windows 和 Linux 下都能够运行, Windows 下使用 Visual Studio 编译,然后在 Linux 下使用 GCC 编译。不同编译器支持的函数可能不同的,现在有个问题,程序中的某个功能,假设 win平台下使用 a(),linux平台下使用 b(),win下的函数在 linux下不能编译通过,反之亦然,怎么办呢?
这就需要在编译之前先对源文件进行处理:
如果检测到是 VS,就保留 a() 删除 b();如果检测到是 GCC,就保留 b() 删除 a()。

#include <stdio.h>

int main() {
   //不同的平台下调用不同的函数
   #if _WIN32  //识别windows平台
   a();
   #elif __linux__  //识别linux平台
   b();
   #endif
  return 0;
}

2、#include命令的使用
#include叫做文件包含命令,用来引入对应的头文件(.h文件)。#include 也是C语言预处理命令的一种。

#include 的处理过程很简单,就是将头文件的内容插入到该命令所在的位置,从而把头文件和当前源文件连接成一个源文件,这与复制粘贴的效果相同。
#include 的用法有两种,如下所示:

#include <stdHeader.h>
#include "myHeader.h"

使用尖括号< >和双引号" "的区别在于头文件的搜索路径不同:
1)使用尖括号< >,编译器会到系统路径下查找头文件;
2)而使用双引号" ",编译器首先在当前目录下查找头文件,如果没有找到,再到系统路径下查找。
   也就是说,使用双引号比使用尖括号多了一个查找路径,它的功能更为强大。一般的情况下,我们引入系统头文件就用<>,引入我们自己编程的头文件就用“”。
关于 #include 用法的注意事项
   1)一个 #include 命令只能包含一个头文件,多个头文件需要多个 #include 命令。
   2)同一个头文件可以被多次引入,多次引入的效果和一次引入的效果相同,因为头文件在代码层面有防止重复引入的机制。一般情况下系统头文件,都可以重复引入,不得出错。同时也要注意,我们自己编写头文件的时候,也要做防止重入引入的机制。
   3)文件包含允许嵌套,也就是说在一个被包含的文件中又可以包含另一个文件。
#include 用法举例
   引入系统头文件没什么好讲的,这里我们玩玩引入自己编写的头文件,编写三个文件main.c;my.h;my.c:
my.c 所包含的代码:

int a = 100;  //申明和定义和赋值一体,对应OS会分配内存空间
//计算从m加到n的和
int sum(int m, int n) {
    int i, sum = 0;
    for (i = m; i <= n; i++) {
        sum += i;
    }
    return sum;
}

my.h 所包含的代码:

//声明变量
extern int a;  //只是申明:OS不会分配内存空间!
//声明函数
extern int sum(int m, int n); //没有函数体,同样OS不会分配内存空间

main.c 所包含的代码:

#include <stdio.h>
#include "my.h"
int main() {
    printf("%d  %d\n", a,sum(1, 100));
    return 0;
}
//执行结果:
//编译
gcc *.c    
./a
//结果
100   5050

注意:该三个文件要放在一个目录下,不能gcc -g main.c -o main.exe,不然会出现undefined reference to sum' C:\Users\ADMINI~1\AppData\Local\Temp\ccU25TQp.o:main.c:(.rdata$.refptr.a[.refptr.a]+0x0): undefined reference toa' 错误,要gcc *.c
运行---启动调试,注意的问题与处理方法:
      在当前这个目录有多文件并需要其它文件包进来时,我们打开一个文件如main.c,再启动调是有问题的,打不开调试终端,这样我们也没办法gcc了,

image.png

处理方法:
   打开另一个文件,没有包含(include)文件的c文件,然后,启动调试,保证打开了终端,我们在终端中再切换到main.c所在的目录中,gcc *.c
image.png

   我们在 my.c 中定义了 sum() 函数,在 my.h 中声明了 sum() 函数,大多数情况下头文件里只能包含变量和函数的声明,不能包含定义,定义都是放在别的源文件里做。

C语言中的宏定义

#define 叫做宏定义命令,它也是C语言预处理命令的一种。
所谓宏定义,就是用一个标识符来表示一个字符串,如果在后面的代码中出现了该标识符,那么就在预处理阶段全部替换成指定的字符串。
我们先通过一个例子来看一下 #define 的用法:

#include <stdio.h>
#define N 100  //前面讲的符号常量,本质就是宏,宏这个字符串不需要用双引号,这个是特殊的
int main(){
    int sum = 20 + N;
    printf("%d\n", sum);
    return 0;
}

宏定义的一般形式为:
#define 宏名 字符串
#表示这是一条预处理命令,所有的预处理命令都以 # 开头。
宏名是标识符的一种,命名规则和变量相同。字符串可以是数字、表达式、if 语句、函数等。

这里所说的字符串是一般意义上的字符序列,不要和C语言中的字符串等同,它不需要双引号。

程序中反复使用的表达式就可以使用宏定义,例如:

#include <stdio.h>
#define M (n*n+3*n)
int main(){
    int sum, n;
    printf("Input a number: ");
    scanf("%d", &n);
    sum = 3*M+4*M+5*M;
    printf("sum=%d\n", sum);
    return 0;
}

程序的开头首先定义了一个宏 M,它表示 (nn+3n) 这个表达式。
在7 行代码中使用了宏 M,预处理程序将它展开为下面的语句:

sum=3*(n*n+3*n)+4*(n*n+3*n)+5*(n*n+3*n);

需要注意的是,在宏定义中表达式(nn+3n)两边的括号不能少,否则在宏展开以后可能会产生歧义。
下面是一个反面的例子:

#difine M n*n+3*n

在宏展开后将得到下述语句:

s=3*n*n+3*n+4*n*n+3*n+5*n*n+3*n;

   和原来预想的表达式,就相差很远了,所以进行宏定义时要注意,应该保证在宏替换之后不发生歧义。
对 #define 用法的几点说明

  1. 宏定义是用宏名来表示一个字符串,在宏展开时又以该字符串取代宏名,这只是一种简单粗暴的替换。
    字符串中可以含任何字符,它可以是常数、表达式、if 语句、函数等,预处理程序对它不作任何检查,
    如有错误,只能在编译已被宏展开后的源程序时发现。

  2. 宏定义不是说明或语句,在行末不必加分号,如加上分号则连分号也一起替换。

  3. 宏定义必须写在函数之外,其作用域为宏定义命令起到源程序结束。
    如要终止其作用域可使用#undef命令。例如:

#define PI 3.14159
int main(){
    // Code
    return 0;
}
#undef PI

void func(){
    // Code
}
  1. 代码中的宏名如果被引号包围,那么预处理程序不对其作宏代替,例如:
#include <stdio.h>
#define OK 100
int main(){
    printf("OK\n");
    return 0;
}

该例中定义宏名 OK 表示 100,但在 printf 语句中 OK 被引号括起来,因此不作宏替换,而作为字符串处理。

表示 PI 只在 main() 函数中有效,在 func() 中无效。

  1. 宏定义允许嵌套,在宏定义的字符串中可以使用已经定义的宏名,在宏展开时由预处理程序层层代换。
    例如:
#define PI 3.1415926
#define S PI*y*y    /* PI是已定义的宏名*/

对语句:

printf("%f", S);

在宏代换后变为:

printf("%f", 3.1415926*y*y);
  1. 习惯上宏名用大写字母表示,以便于与变量区别。但也允许用小写字母。

  2. 可用宏定义表示数据类型,使书写方便。例如:

#define UINT unsigned int

在程序中可用 UINT 作变量说明:

UINT a, b;

讲到#define简化数据类型的标记,不得不联想到另一个关键字:typedef,它的作用是为一种数据类型定义一个新名字,但是它和#define是本质的不同的:
宏定义只是简单的字符串替换,由预处理器来处理;而 typedef 是在编译阶段由编译器处理的,它并不是简单的字符串替换,而给原有的数据类型起一个新的名字,将它作为一种新的数据类型。

另外:typedef本身是一种存储类的关键字,与auto、extern、static、register等关键字不能一起使用。

例子:

#define PIN1 int *
typedef int *PIN2;

从形式上看这两者相似, 但在实际使用中却不相同。

下面用 PIN1,PIN2 说明变量时就可以看出它们的区别:

PIN1 a, b;

在宏代换后变成:

int * a, b;

表示 a 是指向整型的指针变量,而 b 是整型变量。
然而:

PIN2 a,b;

表示 a、b 都是指向整型的指针变量。
因为 PIN2 是一个新的、完整的数据类型。
由这个例子可见,宏定义虽然也可表示数据类型, 但毕竟只是简单的字符串替换。
在使用时要格外小心,以避出错。
8)宏定义是可以包含多条语句的,要换行用\

#include <stdio.h>
#define MYCODE int  n=0;int sum=0;\
         printf("Input a number:  ");\
         scanf("%d",&n);\
         sum = n*n+3*n;\
         printf("sum=%d\n", sum);

int main(){
    MYCODE
    return 0;
}

C语言带参数宏定义

C语言允许宏带有参数。
在宏定义中的参数称为“形式参数”,在宏调用中的参数称为“实际参数”,这点和函数有些类似。

对带参数的宏,在展开过程中不仅要进行字符串替换,还要用实参去替换形参。

带参宏定义的一般形式为:
#define 宏名(形参列表) 字符串 在字符串中可以含有各个形参。

带参宏调用的一般形式为:
宏名(实参列表)
例如:

#include <stdio.h>
#define M(y) (y*y+3)
int main(){
    int k = M(5);
    return 0;
}
//执行编译
 gcc -E define2.c > m001.i
//我们打开m001.i,到该文件尾部可看到宏展开的代码
   // # 3 "define2.c"
    //   int main(){
   //       int k = (5*5 +3);
    //      return 0;
  //      }

在宏展开时,用实参 5 去代替形参 y,经预处理程序展开后的语句为k=(55+35)。

【示例】输出两个数中较大的数。

#include <stdio.h>
#define MAX(a,b) (a>b) ? a : b
int main(){
    int x , y, max;
    printf("input two numbers: ");
    scanf("%d %d", &x, &y);
    max = MAX(x, y);
    printf("max=%d\n", max);
    return 0;
}

对带参宏定义的说明

  1. 带参宏定义中,形参之间可以出现空格,但是宏名和形参列表之间不能有空格出现。
    例如把:
    #define MAX(a, b) (a>b)?a:b
    不可以写为:
    #define MAX (a,b) (a>b)?a:b
  1. 在带参宏定义中,不会为形式参数分配内存,因此不必指明数据类型。
    而在宏调用中,实参包含了具体的数据,要用它们去替换形参,因此实参必须要指明数据类型。这一点和函数是不同的:在函数中,形参和实参是两个不同的变量,都有自己的作用域,
    调用时要把实参的值传递给形参;而在带参数的宏中,只是符号的替换,不存在值传递的问题。
    【示例】输入 n,输出 (n+1)^2 的值。
#include <stdio.h>
#define SQ(y) (y)*(y)
int main(){
    int a, sq;
    printf("input a number: ");
    scanf("%d", &a);
    sq = SQ(a+1);
    printf("sq=%d\n", sq);//如果输入8,结果:81,说明可以调用参数值改变了,不存函数中的值传递问题 
    return 0;
}
  1. 在宏定义中,字符串内的形参通常要用括号括起来以避免出错。
    例如上面的宏定义中 (y)*(y) 表达式的 y 都用括号括起来,因此结果是正确的。
    如果去掉括号,把程序改为以下形式:
#include <stdio.h>
#define SQ(y) y*y
int main(){
    int a, sq;
    printf("input a number: ");
    scanf("%d", &a);
    sq = SQ(a+1);
    printf("sq=%d\n", sq);//sq=a+1*a+1;如果输入8,结果:无
    return 0;
}

注意:带参数的宏和函数很相似,但有本质上的区别,宏展开仅仅是字符串的替换,不会对表达式进行计算;宏在编译之前就被处理掉了,它没有机会参与编译,也不会占用内存。而函数是一段可以重复使用的代码,会被编译,会给它分配内存,每次调用函数,就是执行这块内存中的代码。

宏参数的字符串化和宏参数的连接

在宏定义中,有时还会用到#和##两个符号,它们能够对宏参数进行操作。

#用来将宏参数转换为字符串,也就是在宏参数的开头和末尾添加引号。
例如有如下宏定义:

#define STR(s) #s

那么:

printf("%s", STR(helloworld));    //这里没有双引号
printf("%s", STR("helloworld"));  //这里有双引号

在预处理时分别被替换为:

printf("%s", "helloworld");
printf("%s", "\"helloworld\"");  //多了转义字符 

使用#会在两头添加新的引号,而原来的引号会被转义。
##称为连接符,用来将宏参数或其他的串连接起来。
例如有如下的宏定义:

#define CON1(a, b) a##e##b
#define CON2(a, b) a##b##00

那么:

printf("%f\n", CON1(8.5, 2));
printf("%d\n", CON2(12, 34));

将被展开为:

printf("%f\n", 8.5e2); //850.000000
printf("%d\n", 123400);

C语言中几个预定义宏

ANSI发布的C语言标准里规定了以下几个预定义宏,它们在各个编译器下都可以使用:

__LINE__:表示当前源代码的行号;
__FILE__:表示当前源文件的名称;
__DATE__:表示当前的编译日期;
__TIME__:表示当前的编译时间;
__STDC__:当要求程序严格遵循ANSI C标准时该标识被赋值为1;
__cplusplus__:当编写C++程序时该标识符被定义。
#include <stdio.h>
#include <stdlib.h>
int main() {
    printf("Date : %s\n", __DATE__);
    printf("Time : %s\n", __TIME__);
    printf("File : %s\n", __FILE__);
    printf("Line : %d\n", __LINE__);
    system("pause");
    return 0;
}
//执行结果
PS C:\Users\Administrator\Desktop\workspaceC\yichulimingling> gcc -g define6.c -o define6.exe
PS C:\Users\Administrator\Desktop\workspaceC\yichulimingling> ./define6
Date : Apr 17 2024
Time : 11:48:14   
File : define6.c  
Line : 7

C语言条件编译
假如现在要开发一个C语言程序,让它输出红色的文字,并且要求跨平台,在 Windows 和 Linux 下都能运行,怎么办呢?

这个程序的难点在于,不同平台下控制文字颜色的代码不一样,我们必须要能够识别出不同的平台。

windows平台上:

#include <stdio.h>
#include <windows.h>

int main(int argc, char *argv[]) {
    HANDLE hconsole;  // HANDLE:是一个类型,顾名思义是一个持有资源的句柄
    hconsole = GetStdHandle(STD_OUTPUT_HANDLE);  // 这句话的意思,获取一个持有标准输出的资源句柄
    // SetConsoleTextAttribute函数作用设置前进色和背景色
    SetConsoleTextAttribute(hconsole,0x4a);  //0x4a:4表示背景色,a表示前景色
    printf("Hello,World!\n");

    return 0;
}

//颜色值:
 0 = 黑色       8 = 灰色
 1 = 蓝色       9 = 淡蓝色
 2 = 绿色       A = 淡绿色
 3 = 湖蓝色     B = 淡浅绿色
 4 = 红色       C = 淡红色
 5 = 紫色       D = 淡紫色
 6 = 黄色       E = 淡黄色
 7 = 白色       F = 亮白色
image.png

linux平台上:

printf("\033[字背景颜色;字体颜色m字符串\033[0m" );
printf("\033[47;31mHello,World!\n\033[0m");

//47是字背景颜色, 31m是字体的颜色, Hello,World!\n是字符串\033[0m是控制码.

//部分颜色代码:
//字背景颜色: 40--49               // 字颜色: 30--39
40: 黑                           30: 黑
41: 红                           31: 红
42: 绿                           32: 绿
43: 黄                           33: 黄
44: 蓝                           34: 蓝
45: 紫                           35: 紫
46: 深绿                         36: 深绿
47:白色                         37:白色

Windows 有专有的宏_WIN32,Linux 有专有的宏linux,以现有的知识,我们很容易就想到了 if else,
预处理命令里也有分支语句:

#include <stdio.h>
int main(){
     #if _WIN32
        HANDLE hconsole; 
        hconsole = GetStdHandle(STD_OUTPUT_HANDLE);  
        SetConsoleTextAttribute(hconsole,0x7a);  //0x4a:4表示背景色,a表示前景色
        printf("Hello,World!\n");
     #elif __linux__
        printf("\033[47;42mHello,World!\n\033[22;30m");
    #else
        printf("Hello,World!\n");
    #endif

    return 0;
}

这种能够根据不同情况编译不同代码、产生不同目标文件的机制,称为条件编译。
条件编译是预处理程序的功能,不是编译器的功能。
#ifdef 的用法
   #ifdef 用法的一般格式为:

#ifdef  宏名  //当前的宏已被定义过
    程序段1
#else
    程序段2
#endif

例:如果定义了宏NUM1,则处理,没有则另处理。

#include <stdio.h>
#define NUM1 10
int main(){
    #ifdef NUM1  //改:#ifdef NUM3,改: #ifndef 看看结果,这对判断宏有没有定义很管用。
        //代码A
        printf("NUM1: %d\n", NUM1);
    #else
        //代码B
        printf("Error\n");
    #endif
    return 0;
}

#if、#ifdef、#ifndef三者之间的区别
#if 后面跟的是“整型常量表达式”,而 #ifdef 和 #ifndef 后面跟的只能是一个宏名,不能是其他的。

例如,下面的形式只能用于 #if:

#include <stdio.h>
#define NUM 10
int main(){
    #if NUM == 10 || NUM == 20
        printf("NUM: %d\n", NUM);
    #else
        printf("NUM Error\n");
    #endif
    return 0;
}

再如,两个宏都存在时编译代码A,否则编译代码B:

#include <stdio.h>
#define NUM1 10
#define NUM2 20
int main(){
    #if (defined NUM1 && defined NUM2)
        //代码A
        printf("NUM1: %d, NUM2: %d\n", NUM1, NUM2);
    #else
        //代码B
        printf("Error\n");
    #endif
    return 0;
}

C语言#error命令

#error 指令用于在编译期间产生错误信息,并阻止程序的编译,其形式如下:

#error error_message

例如:

#include <stdio.h>

int main() {
    #ifdef _WIN32
        #error this program cannot compile at win plateform!
    #endif
    return 0;
}

需要注意的是:报错信息不需要加引号" ",如果加上,引号会被一起输出。

#pragma once

#pragma指令的作用是:用于指定计算机或操作系统特定的编译器功能。
#pragma有很多种用法,但确实很偏,这里我们先了解一下#pragma once,以后的开发中遇到其他用法,现查就是了!
#pragma once的作用:保证头文件只被include一次,从而防止变量、函数的重复声明,看例子:

//t1.h
#pragma once
int a;

//t2.h
#include "t1.h"

//main.c
#include "t1.h"
#include "t2.h"

int main(){
    return 0;
}
//预处理
gcc -E main.c >main.i //产生main.i文件,打开,定位到文件最后,可以看到,处理两次int a

注意:#pragma once指令防止重复编译是针对某一个源文件而言的,
即某个源文件包含了两个头文件,而那两个头文件之一又包含了另一个,
此时#pragma once会发生作用,起到只编译一次的作用。

定义宏的时候防止重复定义,一般加#ifndef判断:

#ifndef _X_H
    #define _X_H
#endif 

头文件的编写

多文件的C语言程序的源文件到执行,大致要经历的过程:

clipboard.png

1)强符号和弱符号
   我们在编写代码的过程中经常会遇到一种叫做符号重复定义(Multiple Definition)的错误,这是因为在多个源文件中定义了名字相同的全局变量,并且都将它们初始化了。
例如,在 a.c 中定义了全局变量 global:

int global = 10;

在 b.c 中又对 global 进行了定义:

int global = 20;

那么在链接时就会报错。

    在C语言中,链接器默认
函数和初始化了的全局变量为强符号(Strong Symbol),
            未初始化的全局变量为弱符号(Weak Symbol)。

链接器会按照如下的规则处理被多次定义的强符号和弱符号:

  1. 不允许强符号被多次定义;如果有多个强符号,那么链接器会报符号重复定义错误。
  2. 如果一个符号在某个目标文件中是强符号,在其他文件中是弱符号,强符号覆盖弱符号。
  3. 如果一个符号在所有的目标文件中都是弱符号,那么选择其中占用空间最大的一个。
    比如目标文件 a.o 定义全局变量 global 为 int 类型,占用4个字节,,占用4个字节,
    目标文件 b.o 定义 global 为 double 类型,占用8个字节,那么被链接后,符号 global 占用8个字节。
    请尽量不要使用多个不同类型的弱符号,否则有时候很难发现程序错误。

在 GCC 中,可以通过attribute((weak))来强制定义任何一个符号为弱符号。

#include <stdio.h>

extern int ext;   //表示这个变量,只是声明,不是定义,extern关键字表示此变量定义在了其它文件中了,只是引用
int weak;  //弱弱符号
int strong = 100 ; //strong是强符号,因为它即定底子,又有初值
__attribute__ ((weak)) int weak2=2;   //此处weak2虽然定义也有初值,但被__attribute__转成了弱符号了
int main(){
    return 0;
}

    weak1 和 weak2 是弱符号,strong 和 main 是强符号,而 extern修饰的ext既非强符号也非弱符号,它是一个对外部变量的引用(使用)。

为了加深理解,我们不妨再来看一个多文件编程的例子。
main.c 源码:

#include <stdio.h>
//弱符号
__attribute__((weak)) int a = 20;
__attribute__((weak)) void foo(){
    printf("main c file\n");
}
int main(){
    printf("a = %d\n", a);
    foo();
    return 0;
}

a.c 源码:

#include <stdio.h>
int a = 20;   //a 即有初始值,为强符号了,在整个项目中都优先用,会覆盖其它文件中定义的,没有初值的a
 void foo(){   //foo也是强符号
    printf("a.c file foo function\n");
 }

      在 main.c 中,a 和 func 都是弱符号,在 module.c 中,a 和 func 都是强符号,强符号会覆盖弱符号,所以链接器最终会使用 module.c 中的符号。

      需要注意的是,attribute((weak))只对链接器有效,对编译器不起作用,编译器不区分强符号和弱符号,只要在一个源文件中定义两个相同的符号,不管它们是强是弱,都会报“重复定义”错误。

#include <stdio.h>
__attribute__((weak)) int a = 20;
int a = 9999;
int main(){
    printf("a = %d\n", a);
    return 0;
}

执行与结果

PS C:\Users\Administrator\Desktop\workspaceC\yichulimingling\strong_weaksign> gcc *.c
PS C:\Users\Administrator\Desktop\workspaceC\yichulimingling\strong_weaksign> ./a
20
a.c file foo function

      在 main.c 中,a 和 func 都是弱符号,在 module.c 中,a 和 foo 都是强符号,强符号会覆盖弱符号,所以链接器最终会使用 a.c 中的符号。

      需要注意的是,attribute((weak))只对链接器有效,对编译器不起作用,编译器不区分强符号和弱符号,只要在一个源文件中定义两个相同的符号,不管它们是强是弱,都会报“重复定义”错误。

#include <stdio.h>
__attribute__((weak)) int a = 20;
int a = 9999;
int main(){
    printf("a = %d\n", a);
    return 0;
}

这段代码在编译阶段就会报错,编译器会认为变量 a 被定义了两次,属于重复定义。
2)强引用和弱引用
所谓引用(Reference),是指对符号的使用。

int a = 100, b = 200, c;
c = a + b;

第一行是符号定义,第二行是符号引用。

      目前我们所看到的符号引用,在所有目标文件被链接成可执行文件时,它们的地址都要被找到,如果没有符号声明,链接器就会报符号未声明错误,这种被称为强引用(Strong Reference)。
前面有例子,强引用时, 我们用 #include "xxx.h文件" 该文件中声明了强引用的符号了,这里我们在当前文件中也声明了(即使是弱声明也是声明了呀)。

   弱引用(Weak Reference)同一个文件,如果符号有声明,就正常使用,如果没有声明,也不报错。
先在变量或函数声明的前面加上attribute((weak)),下面在用,符号的引用就变为弱引用:

#include <stdio.h>
__attribute__((weak)) int b=2;    //a强制成弱符号了
__attribute__((weak)) void foo2(){
    printf("main.c file foo function!\n");
}

int main(){
    printf("%d \n",b);
    foo2();
    return 0;
}
//执行与结果
PS C:\Users\Administrator\Desktop\workspaceC\yichulimingling\strong_weaksign> gcc *.c
PS C:\Users\Administrator\Desktop\workspaceC\yichulimingling\strong_weaksign> ./a
2
main.c file foo function!

      弱符号和弱引用对于库来说十分有用,我们在开发库时,可以将某些符号定义为弱符号,这样就能够被用户定义的强符号覆盖,从而使得程序可以使用自定义版本的函数,用可以使用软件本身的函数,增加了很大的灵活性。
      其次,外面在调用某些函数时候用弱引用,被调用的模块,可以很方便地进行裁剪和组合。裁剪掉部分模块顶多让功能弱一些,程序不会报错。

C语言模块化编程中头文件编写的原则

      前面我们在演示多文件编程时创建了 main.c 和 a.c 两个源文件,并在a.c 中定义了一个函数和一个全局变量,然后在 main.c 中进行了弱声明。
      不过实际开发中很少这样做,一般是将函数和变量的声明放到头文件,再在当前源文件中 #include 进来。如果变量的值是固定的,最好使用宏来代替。下面的例子是改进后的代码。
my.h源码

#define OS "Windows 10"   //定义宏
extern int a;
extern void foo();

main.c源文件

#include <stdio.h>
//__attribute__((weak)) int b=2;    //a强制成弱符号了
//__attribute__((weak)) void foo2(){
 //   printf("main.c file foo function!\n");
//}
#include "my.h"
int main(){
    printf("%d \n",a);
    foo();
    printf("OS: %s\n", OS);
    return 0;
}

.h 和 .c 在项目中承担的角色不一样:
.c 文件主要负责实现,也就是定义函数和变量;
.h 文件主要负责声明(包括变量声明和函数声明)、宏定义、类型定义等。
      这些不是C语法规定的内容,而是约定成俗的规范,或者说是长期形成的事实标准。

根据这份规范,头文件可以包含如下的内容:
(1)可以声明函数,但不可以定义函数。
(2)可以声明变量,但不可以定义变量。
(3)可以定义宏,包括带参的宏和不带参的宏。
(4)结构体的定义、自定义数据类型一般也放在头文件中。

         在项目开发中,我们可以将一组相关的变量和函数定义在一个 .c 文件中,并用一个同名的 .h 文件(头文件)进行声明,其他模块如果需要使用某个变量或函数,那么引入这个头文件就可以。

比较规范的C语言多文件编程的结构

clipboard (1).png
#include <stdio.h>
#include "./include/login.h"
#include "./include/reg.h"
#include "./include/tools.h"
int main(){
    return 0;
}

执行:gcc main.c ./module/*.c -o main.exe

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,682评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,277评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,083评论 0 355
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,763评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,785评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,624评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,358评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,261评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,722评论 1 315
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,900评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,030评论 1 350
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,737评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,360评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,941评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,057评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,237评论 3 371
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,976评论 2 355

推荐阅读更多精彩内容