-----我们知道,数组中的所有元素在内存中是连续排列的,如果一个指针指向了数组中的某个元素,那么加 1 就表示指向下一个元素,减 1 就表示指向上一个元素,这样指针的加减运算就具有了现实的意义,这个有意义的应用我们后面在具体写demo来研究。
------不过C语言并没有规定变量的存储方式,如果连续定义多个变量,它们有可能是挨着的,也有可能是分散的,这取决于变量的类型、编译器的实现以及具体的编译模式,所以对于指向普通变量的指针,做加减运算没有意义,因为不知道它后面指向的是什么数据。
------指针变量除了可以参与加减运算,还可以参与比较运算。当对指针变量进行比较运算时,比较的是指针变量本身的值,也就是数据的地址。如果地址相等,那么两个指针就指向同一份数据,否则就指向不同的数据。
-----另外需要说明的是,不能对指针变量进行乘法、除法、取余等其他运算,除了会发生语法错误,也没有实际的含义。
4、数组指针
数组(Array)是一系列具有相同类型的数据的集合,每一份数据叫做一个数组元素(Element)。数组中的所有元素在内存中是连续排列的,整个数组占用的是一块内存。以int arr[] = { 99, 15, 100, 888, 252 };为例,该数组在内存中的分布如下图所示:
定义数组时,要给出数组名和数组长度,数组变量名可以被认为就是一个指针,它指向数组的第 0 个元素。在C语言中,我们将第 0 个元素的地址称为数组的首地址。以上面的数组为例,下图是 arr 的指向:
结合我们前面分析的指针变量的加减运算,循环遍历数组元素,我们还可以这样干:
#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)】计算数组长度.
总结访问数组元素的方法:
使用下标
arr[i] 的形式访问数组元素。如果 p 是指向数组 arr 的指针,也可以使用 p[i] 来访问数组元素。使用指针
使用 *(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;
}
linux上
到底使用字符数组还是字符串常量
在编程过程中如果只涉及到对字符串的读取,那么字符数组和字符串常量都能够满足要求;
如果有写入(修改)操作,那么只能使用字符数组,不能使用字符串常量。
典型的选择字符数组的场景:获取用户输入的字符串,只能使用字符数组:
#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 的指针变量,它们的关系如下图所示:
将这种关系转换为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 的分布是一维线性的,整个数组占用一块连续的内存:
另外,逻辑上二维数组是二维的,但是本质上C语言允许把一个二维数组分解成多个一维数组来处理。
int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };
总结:数组指针,指针数组,二维数组指针!
数组指针:指针变量指向一维数组中某个元素,这个指针变量就是数组指针。
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 来访问二维数组中的每个元素
- p指向数组 a 的开头,也即第 1 行,下标为0;p+1前进一行,指向第 2 行,下标为1。
- *(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函数的参数的时候,代表的是整体的数组本身,
当我们把数组变量名和上面的二维数组指针放在表达式里的时候,代表是就是一个具体的指针(就是一个内存地址)
- *(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;
}
指针的总结
怎么样对付这些复杂指针:
C语言标准规定,对于一个符号的定义,编译器总是从它的名字开始读取,然后按照优先级顺序依次解析。注意,从名字开始,不是从开头也不是从末尾,这是理解复杂指针的关键!
对于初学者,有几种运算符的优先级非常容易混淆,它们的优先级从高到低依次是:
1)定义中被括号( )括起来的那部分。
2)后缀操作符:括号( )表示这是一个函数,方括号[ ]表示这是一个数组。
3)前缀操作符:星号*表示“指向xxx的指针”。
练习理解下面的含有指针的表达式:
int * p1[6];
从 p1 开始理解,它的左边是 *,右边是 [ ],[ ] 的优先级高于 *,所以编译器先解析p1[6],p1 首先是一个拥有 6 个元素的数组,然后再解析int *,它用来说明数组元素的类型。它指针数组。int (p3)[6];
从 p3 开始理解,( ) 的优先级最高,编译器先解析(p3),p3 首先是一个指针,剩下的int [6]是 p3 指向的数据的类型,它是二维数组指针。int (p4)(int a, int b);
从 p4 开始理解,( ) 的优先级最高,编译器先解析(p4),p4 首先是一个指针,它后边的 ( ) 说明 p4 指向的是一个函数,括号中的int, int是参数列表,开头的int用来说明函数的返回值类型。整体来看,p4 是一个指向原型为int func(int, int);的函数的指针,它是函数指针,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);的函数。
- 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 to
a' 错误,要gcc *
.c
运行---启动调试,注意的问题与处理方法:
在当前这个目录有多文件并需要其它文件包进来时,我们打开一个文件如main.c,再启动调是有问题的,打不开调试终端,这样我们也没办法gcc了,
处理方法:
打开另一个文件,没有包含(include)文件的c文件,然后,启动调试,保证打开了终端,我们在终端中再切换到main.c所在的目录中,gcc *.c
我们在 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 用法的几点说明
宏定义是用宏名来表示一个字符串,在宏展开时又以该字符串取代宏名,这只是一种简单粗暴的替换。
字符串中可以含任何字符,它可以是常数、表达式、if 语句、函数等,预处理程序对它不作任何检查,
如有错误,只能在编译已被宏展开后的源程序时发现。宏定义不是说明或语句,在行末不必加分号,如加上分号则连分号也一起替换。
宏定义必须写在函数之外,其作用域为宏定义命令起到源程序结束。
如要终止其作用域可使用#undef命令。例如:
#define PI 3.14159
int main(){
// Code
return 0;
}
#undef PI
void func(){
// Code
}
- 代码中的宏名如果被引号包围,那么预处理程序不对其作宏代替,例如:
#include <stdio.h>
#define OK 100
int main(){
printf("OK\n");
return 0;
}
该例中定义宏名 OK 表示 100,但在 printf 语句中 OK 被引号括起来,因此不作宏替换,而作为字符串处理。
表示 PI 只在 main() 函数中有效,在 func() 中无效。
- 宏定义允许嵌套,在宏定义的字符串中可以使用已经定义的宏名,在宏展开时由预处理程序层层代换。
例如:
#define PI 3.1415926
#define S PI*y*y /* PI是已定义的宏名*/
对语句:
printf("%f", S);
在宏代换后变为:
printf("%f", 3.1415926*y*y);
习惯上宏名用大写字母表示,以便于与变量区别。但也允许用小写字母。
可用宏定义表示数据类型,使书写方便。例如:
#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;
}
对带参宏定义的说明
- 带参宏定义中,形参之间可以出现空格,但是宏名和形参列表之间不能有空格出现。
例如把:
#define MAX(a, b) (a>b)?a:b
不可以写为:
#define MAX (a,b) (a>b)?a:b
- 在带参宏定义中,不会为形式参数分配内存,因此不必指明数据类型。
而在宏调用中,实参包含了具体的数据,要用它们去替换形参,因此实参必须要指明数据类型。这一点和函数是不同的:在函数中,形参和实参是两个不同的变量,都有自己的作用域,
调用时要把实参的值传递给形参;而在带参数的宏中,只是符号的替换,不存在值传递的问题。
【示例】输入 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;
}
- 在宏定义中,字符串内的形参通常要用括号括起来以避免出错。
例如上面的宏定义中 (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 = 亮白色
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语言程序的源文件到执行,大致要经历的过程:
1)强符号和弱符号
我们在编写代码的过程中经常会遇到一种叫做符号重复定义(Multiple Definition)的错误,这是因为在多个源文件中定义了名字相同的全局变量,并且都将它们初始化了。
例如,在 a.c 中定义了全局变量 global:
int global = 10;
在 b.c 中又对 global 进行了定义:
int global = 20;
那么在链接时就会报错。
在C语言中,链接器默认
函数和初始化了的全局变量为强符号(Strong Symbol),
未初始化的全局变量为弱符号(Weak Symbol)。
链接器会按照如下的规则处理被多次定义的强符号和弱符号:
- 不允许强符号被多次定义;如果有多个强符号,那么链接器会报符号重复定义错误。
- 如果一个符号在某个目标文件中是强符号,在其他文件中是弱符号,强符号覆盖弱符号。
- 如果一个符号在所有的目标文件中都是弱符号,那么选择其中占用空间最大的一个。
比如目标文件 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语言多文件编程的结构
#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