六、return关键字
首先,有两个问题引出。
c语言中有字符串类型吗?
c语言中是没有字符串类型的,但是有字符串,JAVA、python、c++是有的。
计算机中如何理解删除数据?
计算机中,删除数据只是把数据设置为无效(可以这么理解,也就是把当前0|1进制的数据全部“注释”掉,可以这么理解,但是不要当作是真的把这些数据注释掉,其实删除数据是文件操控的)。
理解了这两个问题后,我们再来看这段代码:
#include <stdio.h> char *show() { char str[] = { "hello bit" }; return str; } int main() { char* s = show(); printf("%s\n", s); return 0; }
先来看看运行结果:
显然乱码了,那么我们一起来分析一下,为什么乱码了。
首先,我们知道c语言中,函数只要被调用就要向内存开辟空间,下面我们来看看这张图:
看完这张图,我们来整理一下这段代码的思路:首先有main函数,只要是函数就要在栈区开辟空间,那么首先编译器会提前预估main函数中的需要空间大小有多少,然后开辟特定的空间够main函数使用,然后调用show函数,在栈区开辟show函数栈帧,当show函数返回,栈帧释放,数据无效,然后执行main函数中的printf函数,因为show函数栈帧释放了,所以printf函数覆盖了show函数,变成了乱码
怎么证明show函数数据并没有丢失,而是无效呢?下面来看两张监视图便知。
printf函数调用前:
怎么证明show函数数据并没有丢失,而是无效呢?下面来看两张监视图便知。
printf函数调用前:
显然,show函数中的数据并不是清空了,而是无效了被pirntf函数覆盖了。
解释了show函数中的变量都是临时的,那么为什么临时变量具有临时性?
因为,函数调用,形成栈帧;函数返回,栈帧释放。
推荐:return语句不可返回指向“栈内存”的指针,因为该内存在函数体内结束时被自动销毁。
知道了这些,我们来看看返回值临时变量接收的本质。通过一段代码解释。
#include <stdio.h> int GetData() { int x = 0x11223344; printf("run get data!\n"); return x; } int main() { int g = GetData(); printf("g = %x\n", g); return 0; }
运行结果是这样的:
我们就在想,临时变量不是具有临时性吗?不应该直接释放空间,没有返回值吗?下面我们用反汇编来看看是为什么出现这种情况。
来看两站反汇编图就能解决这个问题:
GetData函数中:
mian函数中:
所以,函数返回值是通过寄存器方式返回给函数调用方。
当然,如果不接受也是放在eax寄存器中,并不处理eax寄存器。
七、const关键字
const是干什么的?
是在修饰变量时,为变量附上只读属性。
(一)、const修饰只读变量
#include <stdio.h> int main() { int a = 10; a = 40;//ok const int b = 20;//不可直接被修改 //b = 30;//err printf("%d\n", a); printf("%d\n", b); return 0; }
从上述代码可以看出const修饰变量时,不可直接被修改,但是可以通过指针间接修改。
#include <stdio.h> int main() { const int a = 10; //a = 20;//err int* p = (int*)&a; *p = 20; printf("%d\n", a); return 0; } //输出结果是20
可以通过指针来修改,那么这么说const不就没有意义了吗?
不是的,const的意义:
- 告知编译器这里不能修改,如果修改就会出现报错。
- 告知程序员这里不能修改,具有“自描述”意义。
另外注意一下:
#include <stdio.h> int main() { char* p = "hello world!"; *p = "hello"; printf("%s\n", p); return 0; }
注意这段代码,字符串常量是真正意义上的不能被修改,是被系统约束的。
(二)、const可以作为数组定义的一部分吗?
int mian() { const int a = 10; int arr[a];//err return 0; }
在visual studio 2022中不能被编过的,原因是数组表达式中必须是真正意义上的常量。
(三)、const修饰指针
先通过变量赋值了解指针赋值:
#include <stdio.h> int main() { int a = 20; a = 30;//a指的是空间 printf("%d\n", a);//输出30 return 0; }
再来了解指针赋值(重温:指针变量就是存储地址的空间,指针是地址):
#include <stdio.h> int main() { int a = 20; int b = 10; int* p = &a; p = &b;//p指针变量的空间 int* q = NULL; q = p;//p指的是p存储的地址 printf("%d\n", *p);//输出10 printf("%d\n", *q);//输出10 return 0; }
理解了指针变量和指针的区别,下面我们再来看看const修饰指针的四种情况。
- const int *P;
- int const *p;
- int *const p;
- const int *const p;
- const int *p情况下:
#include <stdio.h> int main() { int a = 10; const int *p = &a; *p = 100;//err p = (int*)100;//ok return 0; }
注意:这里的const修饰的是int *类型(也就是修饰的是指针变量,并不是p这个指针),指的是int *指针空间不能直接被改变,换言之,就是p指针指向的内容不能被改变。p指针是可以被赋值的,所以p=100可以被编译器编过。
- int const *p情况下:
这个和const int *p是一样的,只不过写法不同而已。 - int *const p情况下:
#include <stdio.h> int main() { int a = 10; int *const p = &a; *p = 100;//ok p = (int*)100;//err return 0; }
注意:这里的const修饰的是p这个指针,而不是p这个指针变量,所以p=100是error,p这个指针是不能直接被修改的,这里的int并没有别修饰,所以p这个指针变量是可以被修改的,所以p=100是OK的。
- const int *const p情况下:
#include <stdio.h> int main() { int a = 10; const int* const p = &a; *p = 100;//err p = 100;//err return 0; }
有了上面第一种情况和第三种情况,我们就清楚第四种情况为什么*p和p都不能被修改。
(四)、cosnt修饰函数
1.前言
重温:函数传参就要形成临时变量。
证明:
#include <stdio.h> void show(int* _p) { printf("in_show:%p\n", &_p); } int main() { int a = 10; int* p = &a; printf("int_main:%p\n", &p); show(p); return 0; }
运行结果:
2.const修饰函数返回值
#include <stdio.h> const int* GetValue() { static int a = 10; return &a; } int main() { const int* p = GetValue();//两边类型一样就不会报错 //*p = 100;//err return 0; }
小结:通过这段代码就能很好的解释了const修饰函数返回值,因为GetValue返回的是const修饰的指针类型,所以隐藏含义就是不让其更改。一般内置类型返回加const修饰是没有意义。
八、volatile关键字
volatile关键字是用来干什么的?
编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。
看到这段文字就是懵的。优化是什么?
正常逻辑就是把数据读取到内存中,然后再加载到CPU中的寄存器,进行运算;但是优化是什么,就是不再返回到内存再次读取,而是直接保存在寄存器中,下一次直接运算即可。
(一)、不加volatile
通过这段代码解析:
#include <stdio.h> int pass = 1; int main() { while (pass) { } return 0; }
看一下这段代码在Linux中的反汇编:
(二)、加volatile
#include <stdio.h> volatile int pass = 1; int main() { while (pass) { } return 0; }
再来看看这段代码在Lunix的反汇编:
所以,对比看出来volatile忽略了编译器的优化,保持内存可见性。
(三)、const和volatile关键字的同时使用问题
#include <stdio.h> int main() { const volatile int a = 10; printf("%d\n", a); return 0; }
这里visual studio 2022是不会报错的,原因是const是从写入内存角度,而volatile是从读取内存数据角度谈的,二者不冲突。
九、extern关键字
extern在多文件使用
声明变量、声明函数
extern int a; //声明变量 extern void function();//声明函数
声明函数时不建议舍掉extern,在上一篇中,我们已经说了extern,这里就不再赘述了
十、struct关键字
这里我们不说语法细节部分,我们只是单纯理解一下空结构体多大和柔性数组,后期我们针对结构体再次解释。
(一)、空结构体有多大
#include <stdio.h> struct stu { }; int main() { printf("%d\n", sizeof(struct stu)); return 0; }//直接这样写会出现报错 //在visual studio 2022中提示结构体中必须至少要有一个成员
这是在visual studio 2022中,编译器报错处理。而用gcc编译则是:
打印出来的结果是0。
(二)、柔性数组
柔性数组就是C99中,结构中的最后一个元素允许是未知大小的数组,但是结构中的柔性数组成员前面必须至少有一个其他成员。
介绍了基本柔性数组的概念,我们一起来通过一段代码来知道柔性数组怎么用这个问题。
首先,柔性数组占用空间吗?
通过这个图就能说明柔性数组是不占空间的。
我们再来看看柔性数组是用来干什么的?
#include <stdio.h> struct stu{ int num; //........还有很多的成员,直接省略简化 int arr[0]; }; int main() { int i = 0; struct stu* p = malloc(sizeof(struct stu) + sizeof(int) * 10);//加大空间 p->num = 10; for (i = 0; i < p->num; i++) { p->arr[i] = i; } free(p);//free释放空间 return 0; }
这段代码怎么理解呢?调试起来通过一张图去理解。
这里暂时只说这么多,理解一下即可,后期会对柔性数组再次全面认识
十一、union关键字
这里就不谈语法知识了,后期还会对union进行再一次的认识。
(一)、大小端对于union的影响
#include <stdio.h> union un { int a; char c; }u; int main() { printf("%zd\n", sizeof(union un)); return 0; }//输出结果是4
通过这段代码,我们一起来探讨一下union的内存分布问题:
这个图解释了上面的代码的内存分布,这里有个问题,就是这个char c 空间是在高地址处还是低地址处?
通过打印地址进行检验:
#include <stdio.h> union un { int a; char c; }u; int main() { printf("%p\n", &u); printf("%p\n", &(u.a)); printf("%p\n", &(u.c)); return 0; }
运行结果:
通过这个问题可以看出,每个共用体元素都是第一个元素,每个元素的起始地址都是相同的,都是从低地址处开始的。
再来看看大小端对union共用体的影响,首先还是把代码run起来。
#include <stdio.h> union un { int a; char c; }; int main() { union un x; x.a = 1; if (x.c == 1) { printf("小端\n"); } else { printf("大端\n"); } return 0; }//输出结果是小端
怎么对这里的x.a和x.c分析呢?看图:
小端的话值就是1,大端的话值为0。
(二)、补充
还是一样把代码run起来:
#include <stdio.h> union un { int a; char c[5]; }; int main() { printf("%d\n", sizeof(union un)); return 0; }输出结果是8
这个就很好理解,就是当共用体中空间大小不是定义类型空间大小的倍数的时候,就按共用体中所有类型空间大小的大于整个共用体的空间的最小倍数。例如这里的:整个共用体的空间大小是5,这里面有两个类型int和char,在高于5的情况下,两个类型的最小公倍数是8,所以打印出8。
#include <stdio.h> union un { int i; char a[4]; }*p,u; int main() { p = &u; p->a[0] = 0x39; p->a[1] = 0x38; p->a[2] = 0x37; p->a[3] = 0x36; printf("ox%x\n", p->i); return 0; }//输出结果是ox36373839
这就很简单理解,因为数据储存是以小端方式存储的,a数组和i都是从低地址处进入共用体的空间的,低地址对应的是低权值,所以打印结果是:ox36373839。
十二、enum关键字
enum是枚举关键字,它的作用是什么呢?
列举很多常量,这些常量都是有相关性的。
#include <stdio.h> enum color { RED, YELLOW, WHITE, BLUE, BLACK, GREEN }; int main() { printf("%d\n", RED); printf("%d\n", YELLOW); printf("%d\n", WHITE); printf("%d\n", BLUE); printf("%d\n", BLACK); printf("%d\n", GREEN); return 0; }
看一下这段代码,运行结果是:
012345。所以可以看出枚举常量标识符代表的是一个整型值,并不是字符串。
再来看看这段代码:
#include <stdio.h> enum color { red=3, yellow, white, blue, black=5, green }; int main() { printf("%d\n", red); printf("%d\n", yellow); printf("%d\n", white); printf("%d\n", blue); printf("%d\n", black); printf("%d\n", green); return 0; }
输出结果是:345656。
可以看出,枚举常量整型值是从初始开始依次递增的。
十三、typedef关键字
typedef作用是什么?
本质上就是对类型进行重命名。
(一)、对不同类型的重命名
对一般类型进行重命名
对指针进行重命名
对结构体进行重命名
对数组进行重命名
- 对指针和数组重命名不推荐。
(二)、typedef与#define区别
1.问题一
typedef int * ptr_t;//重新定义新类型,相当于一个独立的类型。 ptr_t p1,p2; //问:p1,p2分别是什么类型?
看图:
看图便知:p1,p2两个变量的类型都是指针类型。
在这里补充一点:
typedef int * ptr_t; int* p1,p2; //问:这里的a,b变量都是一样的类型吗?
看图:
看图便知,p1的类型是int*,p2的类型是int,为什么呢?你可以理解为就近原则吧,p1离 * 更近,所以p1变量是int 指针类型,从本质上来说这是规定,不要问为什么。
这里我们看到int修饰 两个变量时一个是int *,一个是int。那么我们怎么用同一形式定义两个指针呢?
int *p1,*p2;
这样就是定义两个指针类型。
#define PTR_T int* PTR_T p1,p2; //问:p1,p2分别是什么类型?
看图:
看图便知,p1变量是int*指针类型给,p2变量是int类型。和上述的(int * p1,p2;)一个道理。
2.问题二
先看两段代码,再一次理解typedef和define宏定义的区别:
typedef int int32; int main() { unsigned int32 b;//err return 0; }//typedef定义的是一个独立类型,不能再对类型进行修饰
#define INT32 int int main() { unsigned INT32 a;//ok return 0; }
3.typedef static int s_int行不行
不行的。报错显示:指定了一个以上的存储类。
对这个报错显示进行解释:
存储类型关键字有五个:auto、extern、register、static、typedef这五个。存储关键字,不可以同时出现,也就是说,在变量定义的时候只能有一个。