目录
for && do && while && break && continue
数据类型关键字
char(声明字符型变量或函数)
short(声明短整型变量或函数)
int(声明整型变量或函数)
long(声明长整型变量或函数)
signed(声明有符号类型变量或函数)
unsigned(声明无符号类型变量或函数)
float(声明浮点型变量或函数)
double(声明双精度变量或函数)
以上这几类不去细谈,了解即可
重要的是在内存中的存储方式
以下两篇文章细谈了整形和浮点型的存储:大家可以花时间了解一下,对理解内存存储有重要帮助数据的存储-CSDN博客文章浏览阅读34次。我们之前讲过一个变量的创建是要在内存中开辟空间的,空间的大小是根据不同的类型而决定的。那么,数据在所开辟内存中到底是如何存储的呢?我们知道,编译器为a分配四个字节的空间。那如何存储呢?首先,对于有符号数,一定要能表示该数据是 正数还是负数。所以我们一般用最高比特位来进行充当符号位。原码、反码、补码计算机中的有符号数有三种表示方法,即原码、反码和补码。
https://blog.csdn.net/2301_80322352/article/details/134966556浮点数的存储-CSDN博客文章浏览阅读27次。常见的浮点数: 3.14159、1E10等,浮点数家族包括: float、 double、 long double类型。浮点数表示的范围: float.h 中定义上面的代码中,num 和*pFloat在内存中明明是同一个数,为什么浮点数和整数的解读结果会差别这么大?要理解这个结果,一定要搞懂浮点数在计算机内部的表示方法。根据国际标准IEEE (电气和电子工程协会) 754, 任意一个二进制浮点数V可以表示成下面的形式:● (- 1)S表示符号位,当S=0, V为正数;
https://blog.csdn.net/2301_80322352/article/details/134979599
struct(声明结构体变量或函数)
结构是⼀些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
例如描述⼀个学⽣:
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}; //分号不能少
结构体变量的创建和初始化
#include <stdio.h>
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
};
int main()
{
//按照结构体成员的顺序初始化
struct Stu s = { "张三", 20, "男", "20230818001" };
printf("name: %s\n", s.name);
printf("age : %d\n", s.age);
printf("sex : %s\n", s.sex);
printf("id : %s\n", s.id);
//按照指定的顺序初始化
struct Stu s2 = { .age = 18, .name = "lisi", .id = "20230818002", .sex = "女" };
printf("name: %s\n", s2.name);
printf("age : %d\n", s2.age);
printf("sex : %s\n", s2.sex);
printf("id : %s\n", s2.id);
return 0;
}
结构的特殊声明
在声明结构的时候,可以不完全的声明
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;
上⾯的两个结构在声明的时候省略掉了结构体标签(tag)。
那么问题来了?
//在上⾯代码的基础上,下⾯的代码合法吗?
p = &x;
警告:
编译器会把上⾯的两个声明当成完全不同的两个类型,所以是⾮法的。 匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使⽤⼀次。如果不是场景需求,尽量避免匿名的结构体类型定义。
结构体内存对齐
我们已经掌握了结构体的基本使⽤了。
现在我们深⼊讨论⼀个问题:计算结构体的⼤⼩。
⾸先得掌握结构体的对⻬规则:
1. 结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处
2. 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。 对⻬数 = 编译器默认的⼀个对⻬数 与 该成员变量⼤⼩的较⼩值。
VS 中默认的值为 8
Linux中 gcc 没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩
3. 结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的 整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构 体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。
可能单独看这一串话会有点晦涩难懂,我们接下来看一个简单的例子就明白了
struct S1
{
char c1;
int i;
char c2;
};
printf("%d\n", sizeof(struct S1));
这时大部分没有了解结构体内存对齐的人都会认为输出结果为6,因为两个char就是两个字节,int四个字节,加在一起就是6个字节
然而真相就是如此吗
可以看到结果为12,和我们的预期不符,这是为什么呢,我们来画个图就能理解内存对齐的方式了
大家还可以看看这个代码
为什么结构体内容明明一样却大小不同呢
这个时候你就可以之间尝试画图理解
只有自己经过尝试才能更加深刻的了解到内存对齐的方式
为什么存在内存对⻬?
⼤部分的参考资料都是这样说的:
1. 平台原因 (移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定 类型的数据,否则抛出硬件异常。
2. 性能原因: 数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要 作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地 址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以 ⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两 个8字节内存块中。大家看个图就能大概理解了
总体来说:结构体的内存对⻬是拿空间来换取时间的做法。
那在设计结构体的时候,我们既要满⾜对⻬,⼜要节省空间,如何做到
1.让占⽤空间⼩的成员尽量集中在⼀起
2.修改默认对⻬数:#pragma 这个预处理指令,可以改变编译器的默认对⻬数。
#include <stdio.h> #pragma pack(1)//设置默认对⻬数为1 struct S { char c1; int i; char c2; }; #pragma pack()//取消设置的对⻬数,还原为默认 int main() { //输出的结果是什么? printf("%d\n", sizeof(struct S)); return 0; }
结构体在对⻬⽅式不合适的时候,我们可以⾃⼰更改默认对⻬数。
结构体传参
#include<stdio.h>
struct S
{
int data[1000];
int num;
};
struct S s = { {1,2,3,4}, 1000 };
//结构体传参
void print1(struct S s)
{
printf("%d\n", s.num);
}
//结构体地址传参
void print2(struct S* ps)
{
printf("%d\n", ps->num);
}
int main()
{
print1(s); //传结构体
print2(&s); //传地址
return 0;
}
上⾯的 print1 和 print2 函数哪个好些?
答案是:⾸选print2函数。
原因: 函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。 如果传递⼀个结构体对象的时候,结构体过⼤,参数压栈的的系统开销⽐较⼤,所以会导致性能的下 降。
结论: 结构体传参的时候,要传结构体的地址。
结构体实现位段
结构体讲完就得讲讲结构体实现 位段 的能⼒
什么是位段
位段的声明和结构是类似的,有两个不同: 1. 位段的成员必须是 int、unsigned int 或signed int ,在C99中位段成员的类型也可以 选择其他类型。 2. 位段的成员名后边有⼀个冒号和⼀个数字。
//举例
#include<stdio.h>
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
int main()
{
printf("%d\n",sizeof(struct A) );
}
A就是⼀个位段类型。
那位段A所占内存的⼤⼩是多少?
为什么结果时8呢?
2+5+10+30 = 47,那分6字节,也就是48比特不就够了吗.
接下来我们来看看位端的内存分配
位段的内存分配
1. 位段的成员可以是 int unsigned int signed int 或者是 char 等类型
2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的⽅式来开辟的。
3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使⽤位段。
通过上面的话,我们可以来看看为什么结果时8了
首先,系统会先开辟4个字节,不够了再继续开辟,所以当4个字节开辟完后,发现不够存储,会再继续开辟4个字节,所以总共就是8个字节
//⼀个例⼦
struct S
{
char a:3;
char b:4;
char c:5;
char d:4;
};
struct S s = {0};
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
//空间是如何开辟的?
到底是a和b用完一个字节后,系统再开辟一个字节,c是先用a和b用剩下的一个bit位再去新开辟的空间里使用,还是直接就使用新开辟的空间呢
口水话就不多说了,大家直接看图理解,看图可发现,c是直接使用了下个新开辟的空间,而没有用上一次空间剩余下来的空间,所以可以直接得出结论:当⼀个结构包含两个位段,第⼆个位段成员⽐较⼤,⽆法容纳于第⼀个位段剩余的位时,第二个位段会直接使用新开辟的空间。
所以真的就是这样吗?
我们来聊聊位段的跨平台问题
位段的跨平台问题
1. int 位段被当成有符号数还是⽆符号数是不确定的。
2. 位段中最⼤位的数⽬不能确定。(16位机器最⼤16,32位机器最⼤32,写成27,在16位机器会 出问题。
3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
4. 当⼀个结构包含两个位段,第⼆个位段成员⽐较⼤,⽆法容纳于第⼀个位段剩余的位时,是舍弃 剩余的位还是利⽤,这是不确定的。
总结: 跟结构相⽐,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。所以上面的图只是再vs下测试得出来的结论,并不是所有的编译器都是如此,大家要有清晰的认识
位段的应用
下图是⽹络协议中,IP数据报的格式,我们可以看到其中很多的属性只需要几个bit位就能描述,这⾥ 使⽤位段,能够实现想要的效果,也节省了空间,这样网络传输的数据报大小也会较下⼀些,对网络 的畅通是有帮助的。
位段使⽤的注意事项
位段的⼏个成员共有同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位 置处是没有地址的。内存中每个字节分配⼀个地址,⼀个字节内部的bit位是没有地址的。 所以不能对位段的成员使⽤&操作符,这样就不能使⽤scanf直接给位段的成员输⼊值,只能是先输⼊ 放在⼀个变量中,然后赋值给位段的成员。
#include<stdio.h>
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
int main()
{
struct A sa = {0};
scanf("%d", &sa._b);//这是错误的
//正确的⽰范
int b = 0;
scanf("%d", &b);
sa._b = b;
return 0;
}
union(声明共用体(联合)数据类型)
1 联合体类型的声明
像结构体⼀样,联合体也是由⼀个或者多个成员构成,这些成员可以不同的类型。 但是编译器只为最⼤的成员分配⾜够的内存空间。联合体的特点是所有成员共⽤同⼀块内存空间。所 以联合体也叫:共⽤体。
给联合体其中⼀个成员赋值,其他成员的值也跟着变化。
联合体的特点
联合的成员是共⽤同⼀块内存空间的,这样⼀个联合变量的⼤⼩,⾄少是最⼤成员的⼤⼩(因为联合 ⾄少得有能⼒保存最⼤的那个成员)。
代码1输出的三个地址⼀模⼀样,代码2的输出,我们发现将i的第4个字节的内容修改为55了。 我们仔细分析就可以画出,un的内存布局图。
相同成员的结构体和联合体对⽐
我们再对⽐⼀下相同成员的结构体和联合体的内存布局情况。
联合体大小的计算
联合的大小至少是最⼤成员的大小。
当最⼤成员大小不是最⼤对⻬数的整数倍的时候,就要对⻬到最⼤对⻬数的整数倍。
使⽤联合体是可以节省空间的,举例:
比如,我们要搞⼀个活动,要上线⼀个礼品兑换单,礼品兑换单中有三种商品:图书、杯⼦、衬衫。 每⼀种商品都有:库存量、价格、商品类型和商品类型相关的其他信息。
图书:书名、作者、页数
杯⼦:设计
衬衫:设计、可选颜⾊、可选尺⼨
那我们不耐⼼思考,直接写出⼀下结构:
struct gift_list
{
//公共属性
int stock_number;//库存量
double price; //定价
int item_type;//商品类型
//特殊属性
char title[20];//书名
char author[20];//作者
int num_pages;//⻚数
char design[30];//设计
int colors;//颜⾊
int sizes;//尺⼨
};
上述的结构其实设计的很简单,⽤起来也⽅便,但是结构的设计中包含了所有礼品的各种属性,这样 使得结构体的⼤⼩就会偏⼤,⽐较浪费内存。因为对于礼品兑换单中的商品来说,只有部分属性信息 是常⽤的。⽐如:
商品是图书,就不需要design、colors、sizes。
所以我们就可以把公共属性单独写出来,剩余属于各种商品本⾝的属性使⽤联合体起来,这样就可以 介绍所需的内存空间,⼀定程度上节省了内存。
struct gift_list
{
int stock_number;//库存量
double price; //定价
int item_type;//商品类型
union
{
struct
{
char title[20];//书名
char author[20];//作者
int num_pages;//⻚数
}book;
struct
{
char design[30];//设计
}mug;
struct
{
char design[30];//设计
int colors;//颜⾊
int sizes;//尺⼨
}shirt;
}item;
};
enum:声明枚举类型
枚举顾名思义就是⼀⼀列举。
把可能的取值⼀⼀列举。
⽐如我们现实⽣活中: ⼀周的星期⼀到星期⽇是有限的7天,可以⼀⼀列举 性别有:男、⼥、保密,也可以⼀⼀列举 ⽉份有12个⽉,也可以⼀⼀列举 三原⾊,也是可以一 一列举
这些数据的表示就可以使⽤枚举了
enum Day//星期
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Sex//性别
{
MALE,
FEMALE,
SECRET
};
enum Color//颜⾊
{
RED,
GREEN,
BLUE
};
以上定义的 enum Day , enum Sex , enum Color 都是枚举类型。 { }中的内容是枚举类型的可能取值,也叫 枚举常量 。 这些可能取值都是有值的,默认从0开始,依次递增1,当然在声明枚举类型的时候也可以赋初值。
枚举类型的优点
为什么使⽤枚举?
我们可以使⽤ #define 定义常量,为什么⾮要使⽤枚举? 枚举的优点:
1. 增加代码的可读性和可维护性
2. 和#define定义的标识符⽐较枚举有类型检查,更加严谨。
3. 便于调试,预处理阶段会删除 #define 定义的符号
4. 使⽤⽅便,⼀次可以定义多个常量
5. 枚举常量是遵循作⽤域规则的,枚举声明在函数内,只能在函数内使⽤
那是否可以拿整数给枚举变量赋值呢?在C语⾔中是可以的,但是在C++是不⾏的,C++的类型检查比较严格。
void(声明函数无返回值或无参数,声明无类型指针)
//void是否可以定义变量
#include <stdio.h>
int main()
{
void a;
return 0;
}
//在vs2013和Centos 7,gcc 4.8.5下都不能编译通过
为何 void 不能定义变量
定义变量的本质:开辟空间
而void作为空类型,理论上是不应该开辟空间的,即使开了空间,也仅仅作为一个占位符看待 所以,既然无法开辟空间,那么也就无法作为正常变量使用,既然无法使用,编译器干脆不让他定义变量。
在vs2013中,sizeof(void)=0
在Linux中,sizeof(void)=1(但编译器依旧理解成,无法定义变量)
void修饰函数返回值和参数
//场景1
//void用来作为函数返回值
#include <stdio.h>
void show()
{
printf("no return value!\n");
}
int main()
{
show();
return 0;
}
//如果自定义函数,或者库函数不需要返回值,那么就可以写成void
//那么问题来了,可以不写吗?不可以,自定义函数的默认返回值是int(这个现场验证)
//所以,没有返回值,如果不写void,会让阅读你代码的人产生误解:他是忘了写,还是想默认int?
结论:void作为函数返回值,代表不需要,这里是一个"占位符"的概念,是告知编译器和给阅读源代码的工程师看的
//场景2
//void 作为函数参数
//如果一个函数没有参数,我们可以不写, 如test1()
#include <stdio.h>
int test1() //函数默认不需要参数
{
return 1;
}
int test2(void) //明确函数不需要参数
{
return 1;
}
int main()
{
printf("%d\n", test1(10)); //依旧传入参数,编译器不会告警或者报错
printf("%d\n", test2(10)); //依旧传入参数,编译器会告警(vs)或者报错(gcc)
return 0;
}
//结论:如果一个函数没有参数,将参数列表设置成void,是一个不错的习惯,因为可以将错误明确提前发现
//另外,阅读你代码的人,也一眼看出,不需要参数。相当于"自解释"。
//当然,如果不习惯也不勉强。
void 指针
//void不能定义变量,那么void*呢?
#include <stdio.h>
int main()
{
void *p = NULL; //可以
return 0;
}
//为什么void*可以呢?因为void*是指针,是指针,空间大小就能明确出来
//场景
//void* 能够接受任意指针类型
#include <stdio.h>
int main()
{
void *p = NULL;
int *x = NULL;
double *y = NULL;
p = x; //虽然类型不同,但是编译器并不报错
p = y; //同上
return 0;
}
//反过来,在vs/gcc中也没有报错。书中编译器很老了,我们严重不推荐(C语言深度解剖)
//结论:但是我们依旧认为,void*的作用是用来接受任意指针类型的。这块在后面如果想设计出通用接口,很有用
//比如:
//void * memset ( void * ptr, int value, size_t num );
void * 定义的指针变量可以进行运算操作吗
//在vs2013中
#include <stdio.h>
int main()
{
void *p = NULL;
p++; //报错
p += 1; //报错
return 0;
}
//在gcc4.8.5中
#include <stdio.h>
int main()
{
void *p = NULL; //NULL在数值层面,就是0
p++; //能通过
printf("%d\n", p); //输出1
p += 1; //能通过
printf("%d\n", p); //输出2
return 0;
}
//为什么在不同的平台下,编译器会表现出不同的现象呢?
根本原因是因为使用的C标准扩展的问题。具体阅读书。(C语言深度解剖)
我这里有问题:
对指针++具体是怎么++?
为何gcc中,输出结果是 1 和 2 呢?
本质和不同平台,看待void空间大小相关。
补充内容:
GNU计划,又称革奴计划,是由Richard Stallman(理查德·斯托曼)在1983年9月27日公开发起的。它的目是创建一套完全自由的操作系统。它在编写linux的时候自己制作了一个标准成为 GNU C标准。ANSI 美国国家标准协会它对C做的标准ANSI C标准后来被国际标准协会接收成为 标准C 所以 ANSI C 和标准C是一个概念,总体来说现在linux也支持标准C,以后标准C可以跨平台,而GUN c 一般只在linux c下应用。--来自百度
Linux 上可用的 C 编译器是 GNU C 编译器,它建立在自由软件基金会的编程许可证的基础上,因此可以自由发布。 GNUC 对标准 C 进行一系列扩展,以增强标准 C 的功能。--来自百度
一句话,大部分编译器是标准C,而Linux下是扩展C,Linux平台也能保证标准C的运行
控制语句关键字(12个)
1.循环控制(5个)(基本语法不细谈)
for && do && while && break && continue
//while
条件初始化
while(条件判定){
//业务更新
条件更新
}
//for
for(条件初始化; 条件判定; 条件更新){
//业务代码
}
//do while
条件初始化
do{
条件更新
}while(条件判定);
三种循环对应的死循环写法
while(1){
}
for(;;){
}
do{
}while(1);
//书中代码,优化后(C语言深度解剖)
//这个代码输出的时候,会有什么问题?
#include <stdio.h>
int main()
{
while (1){
char c = getchar(); //读取字符函数
if ('#' == c){
break;
}
printf("echo: %c\n", c);
}
return 0;
}
//测试用例1:break:终端输入abcd#1234
//测试用例2:continue:终端输入abcd#1234
#include <stdio.h>
int main()
{
while (1){
char c = getchar();
if ('#' == c){
break; //测试用例1
//continue; //测试用例2
}
printf("echo: %c\n", c);
}
return 0;
}
循环必备的三要素(循环条件初始化,循环条件判定,循环条件更新)
注意:在while和do while中,continue是跳到条件判断处
在for中,continue是跳到条件更新处
2.条件语句(三个)
if && else(只谈关键)
1. if语句的执行流程永远都是先执行()内的语句,得出bool值再判断真假
2. 0为假,非0为真
0为假,1为真这个说法并不准确。
3. if else匹配原则:
else匹配采取就近原则,推荐每次写if 或者else都带上{ }。4. if后加分号:
这个问题初学者很容易犯,因为系统并不报错,
;就代表if执行的是一个空语句;所以从语法来讲并不出错
5. ==误写成=,误写后,如果右边对于的是一个非0常量,if就会永远成立;
goto
基本使用
//使用goto模拟实现循环
#include <stdio.h>
int main()
{
int i = 0;
START:
printf("[%d]goto running ... \n", i);
Sleep(1000);
++i;
if (i < 10){
goto START; //永远都会跳到上面的START
}
printf("goto end ... \n");
return 0;
}
不要对goto有偏见,在一些特殊场景下goto有奇效
像Linux内核源代码中充满了大量的goto,只能说我们目前,或者很多公司的业务逻辑不是那么复杂,所有大多数人都会对goto有偏见
3.开关语句(3个)
switch && case && default
switch(整型变量/常量/整型表达式){
case var1:
break;
case var2:
break;
case var3:
break;
default:
break;
}
已经有if else为何还要switch case
switch语句也是一种分支语句,常常用于多分支的情况。这种多分支,一般指的是很多多分支,而且判定条件主要以整型为主,如:
输入1,输出星期一
输入2,输出星期二
输入3,输出星期三
输入4,输出星期四
输入5,输出星期五
输入6,输出星期六
输入7,输出星期日
如果写成 if else 当然是可以的,不过比较麻烦
#include <stdio.h> int main() { int day = 1; switch (day){ case 1: printf("星期一\n"); break; case 2: printf("星期二\n"); break; case 3: printf("星期三\n"); break; case 4: printf("星期四\n"); break; case 5: printf("星期五\n"); break; case 6: printf("星期六\n"); break; case 7: printf("星期日\n"); break; default: printf("bug!\n"); break; } return 0; }
case的作用是什么?break在switch中的作用是什么?
case本质本质是进行判定功能
break本质其实是进行分支功能
switch本身是没有像if else一样拥有判断,分支功能的
如果多个不同case匹配,想执行同一个语句,推荐做法:
#include <stdio.h>
int main()
{
int day = 6;
switch (day){
case 1:
case 2:
case 3:
case 4:
case 5:
printf("周内\n");
break;
case 6:
case 7:
printf("周末\n");
break;
default:
printf("bug!\n");
break;
}
return 0;
}
结论:case之后,如果没有break,则会依次执行后续有效语句,直到碰到break
case后面的值有什么要求吗?
switch(m) && case n
//其中m 和 n必须是什么类型变量或者表达式?
//case 语句后面是否可以是const修饰的只读变量呢?不行
#include <stdio.h>
int main()
{
const int a = 10;
switch (a){
case a: //不行
printf("hello\n");
break;
default:
break;
}
return 0;
}
case default必须要按顺序放吗?
不是必须,但尽量按顺序放,除非特殊场景。
//default可以放在switch内任意处
总结
4.返回语句(1个)
return
//如何正确理解这段代码
#include <stdio.h>
char* show()
{
char str[] = "hello bit";
return str;
}
int main()
{
char *s = show();
printf("%s\n", s);
return 0;
}
//如何理解栈帧销毁,计算机中所谓的删除数据究竟是在做什么?
//返回值临时变量接收的本质
#include <stdio.h>
int test()
{
int a = 10;
return a; //既然a是函数定义的变量,具有临时性,那么这个临时变量在函数退出的时候,应该被释放
}
int main()
{
int a = test();
printf("%d\n", a);
//test(); //没有变量接收返回值的时候,有返回值吗?
return 0;
}
//通过查看汇编可以看到,对于一般内置类型,寄存器eax可以充当返回值的临时空间
//回答结尾问题:通过查看汇编方式,理解返回值
#include <stdio.h>
//void test()
int test()
{
return 1;
}
int main()
{
int x = test();
//test();
return 0;
}
return是通过寄存器返回值的
//所以如何正确理解这段代码
#include <stdio.h>
char* show()
{
char str[] = "hello bit";
return str;
}
int main()
{
char *s = show();
printf("%s\n", s);
return 0;
}
该程序打印出来的为什么会是乱码呢?(可自行测试)
因为return返回的时候,show函数中的str这个临时数组会被自动释放掉(临时变量具有临时性质的特征),但计算机是不会真正意义上的清空我们的数据,(当然,实际情况会把我们的空间进行一定程度的重新启动,也就是初始化,清空什么的)但现在只需要理解计算机并不会立马清空数据即可,所以str释放掉后,我们依旧可以通过监视看到对应的字符串,但是为什么通过printf函数打印后就是乱码呢,原因在于任何一个函数被调用的时候都会形成一个栈帧,main函数也是函数,所以,main函数本身也有栈帧结构,所谓栈帧结构就是再栈上为对应函数开辟一段小的空间,所以当我们调用printf函数时,就会在main函数的下层,为printf函数形成栈帧,这样就会覆盖show函数曾经我们建立好的栈帧结构,里面的数据也就全部被清空了,所以最终打印出来的是乱码。
存储类型关键字(5个)
auto
如何使用:一般在代码块中定义的变量,即局部变量,默认都是auto修饰的,不过一般省略 默认的所有变量都是auto吗?不是,一般用来修饰局部变量 中断一下:后面我们所到的,局部变量,自动变量,临时变量,都是一回事。我们统称局部变量
#include <stdio.h>
int main()
{
for (int i = 0; i < 10; i++){
printf("i=%d\n", i);
if(1)
{
auto int j = 0; //自动变量
printf("before: j=%d\n", j);
j += 1;
printf("after : j=%d\n", j);
}
}
return 0;
}
i用auto修饰可以吗?去掉的auto可以吗?
答案是都可以,不过现在定义变量其实前面都已经有隐藏的auto了
结论:已经很老,基本用不使用
extern
可以看到,明明在test.c中定义的g_val,为什么main.c也能用呢,原因就在于 extern修饰后,扩大了g_val的作用域,能让其他文件也能看到它
extern我们这里只关注代码风格:
大家以后在多文件情况下是写代码时,在头文件中不管是变量还是函数声明,都要带上extern,虽然有时候没有带上,编译器也不会报错,但带上好处多多,我就不细谈了。
register
其实,CPU主要是负责进行计算的硬件单元,但是为了方便运算,一般第一步需要先把数据从内存读取到CPU内,那 么也就需要CPU具有一定的数据临时存储能力。注意:CPU并不是当前要计算了,才把特定数据读到CPU里面,那样 太慢了。 所以现代CPU内,都集成了一组叫做寄存器的硬件,用来做临时数据的保存。
距离CPU越近的存储硬件,速度越快
寄存器存在的本质
在硬件层面上,提高计算机的运算效率。因为不需要从内存里读取数据。
register 修饰变量
尽量将所修饰变量,放入CPU寄存区中,从而达到提高效率的目的。(并不是说只要修饰了,就一定会放到CPU寄存区中)
那么什么样的变量,可以采用register呢?
1. 局部的(全局会导致CPU寄存器被长时间占用)
2. 不会被写入的(写入就需要写回内存,后续还要读取检测的话,register的意义在哪呢?)
3. 高频被读取的(提高效率所在)
4. 如果要使用,请不要大量使用,因为寄存器数量有限
这里除了上面的,再有一点,就是register修饰的变量,不能取地址(因为已经放在寄存区中了嘛,地址是内存相关的概念)
我的意见:该关键字,不用管,因为现在的编译器,已经很智能了,能够进行比人更好的代码优化。 早期编译器需要人为指定register,来进行手动优化,现在不需要了
static
.h:我们称之为头文件,一般包含函数声明,变量声明,宏定义,头文件等内容(header)
.c: 我们称之为源文件,一般包含函数实现,变量定义等 (.c : c语言)
//test.h
#pragma once //防止头文件被重复包含,当前只需要记住,后面会无数次用
#include <stdio.h>
//test.c
#include "test.h" //""包含头文件,目前只需要知道是自己写的头文件,就用""包含即可
//main.c
#include "test.h" //同上
int main()
{
printf("hello files!\n");
return 0;
}
全局变量和函数的两个结论
1. 全局变量,是可以跨文件,被访问的。
2. 全局函数,是可以跨文件,被访问的
修饰变量
1. 修饰全局变量,该全局变量只能在本文件内被使用。
//总结:static修饰全局变量,影响的是作用域的概念,函数类似。而生命周期是不变的.
2. 修饰局部变量
void fun1()
{
int i = 0;
i++;
printf("no static: i=%d\n", i);
}
void fun2()
{
static int i = 0;
i++;
printf("has static: i=%d\n", i);
}
int main()
{
for (int i = 0; i < 10; i++){
fun1();
fun2();
}
return 0;
}
//结论:static修饰局部变量,变量的生命周期变成全局周期。(作用域不变)
有些误导性
第一个作用:修饰变量。变量又分为全局变量和局部变量,但它们都在内存的静态区。
关于C存储布局,我后面会在函数专题整体来讲
static只能让变量的生命周期变为全局周期,作用域是不变的。
typedef
typedef可以让我们在面对冗长的类型命名方面,可以让我们解脱出来。
但是不要一昧的把所以类型都重命名,目前我只推荐使用结构体的时候使用typedef,其他场景都不太推荐
typedef和宏的区别
#define是完成替换功能,而typedef是定义新的类型 ,会对后面的所有变量全部起效
为什么是新的类型呢,可以举个例子
可以看到typedef不能和其他关键字一起修饰变量,哪怕它们之前是可以一起的。
那这个又是什么原因呢,从报错信息来看,我们可以大概知道static和typedef都是存储类关键字,C语言中不允许同时出现两个存储类型的关键字。
其他关键字(3个)
const
//const修饰变量
#include <stdio.h>
int main()
{
const int i = 10;
i = 20; //报错,代表变量i不可以直接被修改
return 0;
}
//const修饰变量真的不能被修改吗?
#include <stdio.h>
int main()
{
const int i = 10;
int *p = (int*)&i;
printf("before: %d\n", i);
*p = 20;
printf("after: %d\n", i);
return 0;
}
//结论:const修饰的变量并非是真的不可被修改的常量
//那const修饰变量,意义何在?
1. 让编译器进行直接修改式检查
2. 告诉其他程序员(正在改你代码或者阅读你代码的)这个变量后面不要改哦。也属于一种“自描述”含义
//const修饰的变量,可以作为数组定义的一部分吗?
#include <stdio.h>
int main()
{
const int n = 100;
int arr[n];
return 0;
}
//在vs2013(标准C)下直接报错了,但是在gcc(GNU扩展)下,可以
//但我们一切向标准看齐,不可以。
来看一下const在不同位置有何作用
#include<stdio.h>
int main()
{
int a = 0;
const int *p = &a; //表示p指向的空间内容不可更改
int* const p = &a; //表示p的内容不可更改
const int * const p = &a; //表示p所指向的空间和p自身都不可更改
return 0;
}
sizeof
自定义类型的大小也是可以用sizeof去求的
#include <stdio.h>
int main()
{
int a = 0;
printf("%zd\n", sizeof(a)); //正确
printf("%zd\n", sizeof(int)); //正确
printf("%zd\n", sizeof a); //正确
printf("%zd\n", sizeof int); //error
return 0;
}
sizeof是操作符用来求特定类型所占空间大小,而不是函数
volatile
心理建设
//保持内存可见性
//接下来,我在汇编角度,在Linux平台给大家对比演示一下加还是不加volatile的作用,让大家看明白。
//不过可能会有些难度,大家就看一下基本过程就行
[whb@VM-0-3-centos code]$ cat test.c #include <stdio.h> int pass = 1; int main() { while(pass){ //思考一下,这个代码有哪些地方,编译器是可以优化的。 } return 0; } [whb@VM-0-3-centos code]$ gcc test.c -O2 -g //以O2级别进行代码优化 [whb@VM-0-3-centos code]$ objdump -S -d a.out > a.s //对形成的a.out可执行程序进行优化 [whb@VM-0-3-centos code]$ vim a.s //查看汇编代码
加volatile
[whb@VM-0-3-centos code]$ cat test.c #include <stdio.h> volatile int pass = 1; //加上volatile int main() { while(pass){ } return 0; } [whb@VM-0-3-centos code]$ gcc test.c -O2 -g //以O2级别进行代码优化 [whb@VM-0-3-centos code]$ objdump -S -d a.out > aa.s //对形成的a.out可执行程序进行优化 [whb@VM-0-3-centos code]$ vim aa.s //查看汇编代码
结论: volatile 忽略编译器的优化,保持内存可见性
其他问题
const volatile int a = 10;
//在vs2013和gcc 4.8中都能编译通过
//const是在编译期间起效果
//volatile在编译期间主要影响编译器,形成不优化的代码,进而影响运行,故:编译和运行都起效果。
const要求你不要进行写入就可以。volatile意思是你读取的时候,每次都要从内存读。 两者并不冲突。
虽然volatile就叫做易变关键字,但这里仅仅是描述它修饰的变量可能会变化,要编译器注意,并不是它要求对应变量必须变化!这点要特别注意。
C语言中32个关键字也就讲完了,如果大家觉得还不错,对您有帮助的话可以点赞+关注哦!!