阅前须知:此篇博客的大部分内容来自《C语言深度解剖》一书以及网上查阅各个资料还有自己的亲自实践。代码主要运行在linux环境下和VS2013环境下,希望读者看了以后不要照抄,要学会结合自己的思考,本篇博客主要记录了部分关键词拓展或者说是用到该关键词需要注意的地方,说白了就是死命抠细节,不是单纯介绍关键词,所以不一定适合入门的人看。还有!本篇博客内容仅仅局于C语言,有些东西可能放在C++上还有别的含义和作用,笔者不能一一成列实在抱歉。可以把他看做成笔者的学习总结,谢谢审阅,欢迎评价。
一.从static和extern开始。
static的作用比较广泛,主要分在两个方面:
1.修饰变量。
修饰变量也分修饰全局变量和修饰局部变量——前者作用呢是让该变量只能用于被定义的文件中,有点像怕老婆跑了的感觉,就算其他文件用extern声明也不能用,嘿嘿,就是这么酷。
后者则是为了延长变量的生命周期,本来局部变量的生命周期是所在函数一结束,就会被释放,现在不一样了,要等程序结束,他才会嗝屁。
至于为什么?因为局部变量本来是创建在栈里的,但因为加了static之后他就变了身份,他成了一个静态变量,需要把他放在static区域,也就是静态存储区域,这个时候他的命运不再由函数把控,而是把他的命运和应用绑在一起,应用结束的时候他才被释放。(一夜暴富的感觉)
2。修饰函数
修饰函数的作用和修饰全局变量的作用蛮像的,目的就是为了不让别的文件用自己的函数。
——static在平时写代码的时候也比较常用,所以我也不用举例子啥的。如果你说你看不懂上面的extern,没事我们等会就讲了;如果你说你看不懂“生命周期”,没事,以后代码写多了,你和代码就有感情了,代码就是你的第二个老婆啊~老婆怎么会是死气沉沉的;如果你说你看不懂栈(stack)啊,静态区(static)啊,如果你下定决心要学好C语言,这个方面还是要好好学的,了解一个变量存在在内存的位置,了解他的调用过程,还有排列的方式,可以看看我前面那篇博客,当然我觉得网上还有更好的,大家一起努力!
extern的作用主要就是声明外来变量或函数。
务必记住,extern是声明,不是定义,看下面代码:
A文件: B文件: int i=10; extern i; static int tmp=20; extern fun(); int fun () { return tmp; }
当然你也可以试试给B文件加一个“extern tmp”,然后再在B文件里找个什么代码用一下tmp,看看可不可行。我上面虽然讲了,但还是希望你实践一下,万一你那个编译器可以通过呢(开个玩笑,怎么可能)。
这里可以引申几个问题,你都可以回去试一下,我都在这里解答了:
1.extern后面可以跟类型吗?跟了类型会让变量类型强转吗?
答:可以(除了“void”),准确说是必须跟原来的类型,会的。如果改成“extern float i”,i的值就变0了,至于为什么变0,这是因为 浮点数 和 整型以及字符型 数据在内存中的存储方式不同。如果你现在不懂,将来一定要懂!很重要!
2.extern后面的变量在声明时可以给他赋值吗?
答:不可以。其实某种意义上来讲是可以的,因为你是程序员你想怎么输就怎么输,但是编译器会报错,这就又回到了我们上面在“生命周期”提到的代码情感问题了,你想做一件事,但是你老婆不愿意,那你能做吗?当然不行!活腻了这不是。
3.extern声明完变量后再赋值可以吗?
答:可以,必须放在函数里赋值。但是有什么意义呢,干嘛不自己定义一个,而要去用别人的?没必要啊。
二.关于各类基本数据类型。
我们知道常见的基本数据类型无非:short,int,long,char,float,double,long long。
你开始学这一方面的知识的时候,一定要了解这些类型的数据的长度,你问我怎么知道?可以上网查,也可以写个代码用 sizeof(数据类型) , printf 一下,你就知道答案了,sizeof我们一会也会讲,这是个大考点。
看下面代码:
#define _CRT_SECURE_NO_WARNINGS 1 #include#include int main() { char a[1000]; int i; for (i = 0; i<1000; i++) { a[i] = -1 - i; } printf("%d\n", strlen(a)); system("pause"); return 0; }
最后答案是255,为什么不是1000呢?很奇怪吧,其实也不奇怪,记住char只有8位——也就是一个字节,存到256的时候读的时候是个0,strlen有比较笨,它只知道读到0就停下来,所以值为255。
还有关于float,double浮点型数和0的比较,千万千万不要去直接和0比,一定要和0的近似值比!!!
#define PRE 0.00000001 //宏定义一个精度 ... float i =0.1; ... //----------错误↓-------------- if(i=0.0) { ;//code } //----------正确↓--------------- if(i<=PRE&&i>=-PRE) { ;//code } ...
多的也都不写了,这些关键词要注意的就是存储方式还有占空间大小的问题。再多说一点:
养成命名的好习惯,比如一些define宏定义的东西,一些只读变量,一些枚举类型的变量,最好用大写。这个不能教,每个人有个人的习惯,这个时候英语好变得有优势了(欲哭无泪),要注意简洁,也要易懂。推荐一本书《高质量C C++编程》,有讲一些关于命名的东西。
三.sizeof,顺便一提strlen。
记住,sizeof是一个关键字,不是函数。
怎么证明?
int i=10; A)sizeof(int); B)sizeof(i); C)sizeof int; D)sizeof i;
将上面的代码一一放到编译器里尝试,证明都是可以通过的,如果你有了解到C语言的操作符,你会知道操作符里有一个叫做“函数调用”的操作符,说白了就是“()”,所以可以证明,sizeof是关键字不是函数。
需要注明的是,sizeof(字符串)的时候,会把字符串的‘\0’给算进去,所以如果有一个字符串 arr,sizeof(arr) 的值会比 strlen(arr) 大1。
既然都聊到strlen了,我们也就细说一下,strlen 是一个字符串函数,在头文件
看下面代码:
#includeint main()
{ const char*str1 = "abcdef"; const char*str2 = "bbb"; if(strlen(str2)-strlen(str1)>0) { printf("str2>str1\n");
} else { printf("srt1>str2\n");
} return 0; }
最后答案是“str2>str1”,怎么可能??语句2明明比1短啊,你别忘了,一个无符号数减去另一个无符号数,还是一个无符号数,怎么可能小于0呢对吧,觉得神奇吧,还不赶紧拿小本本记下来。
四.if,else和switch,case。
这两者都是判断类型的,是就执行,不是就跳过。
两者都比较常用,各有各的用法,所以各有各的优势。
先说if,else,if和else要注意的点有两个:
1.如果为了判断一个bool类型的数是否为0来当做条件。最好的说法就是:
bool bTest=true; if(!bTest) { ;//code } //或者 bool bTest=true; if(bTest) { ;//code }
PS:bool的数据就两个值——TRUE和FALSE。
2.else对应离他最近的那个if。记住就好,代码写多了,忘也忘不掉。你会忘记你老婆长啥样吗?tx。
再说switch,case,作用和if很像。
既然作用很想,为啥会不被替代呢?他们只是作用像,但是应用上差距还是蛮大的,switch用于一些分支比较多的判断语句,但是他的条件要相对简单。
switch,case也要注意两个点:
1.case后面只能跟整型和字符型的常量或者常量表达式。
2.任何switch语句都要养成好习惯,在结尾加一个:
... default: break; ...
五.关于for,while,do...while。
其实这也没啥好讲的,因为一直在用,需要注意的有,do...while用的时候记得他永远是先执行一次循环,再判断是否有资格进入循环。所以同一个情况下要比其他两个多执行依次循环。
do...while用的频率不高,for是最多的,while主要用于一些需要无限循环的场景。
如果需要无限循环,就这么写:
... while(1) { ;//code } ...
注意,养成好习惯,长的循环放里面,短的循环放外面。循环要小,最多不能超过三层。
六. goto关键字。
goto就是一个比较自由的关键字,可以灵活跳转,但容易产生错误与隐患。
七.void型的函数和void*型的指针变量。
void说白了,就是空。但人家也不是真的空。
void的变量是没意义的,所以直接可以当不存在。void类型的函数还真的有,比如什么交换函数,就是没有返回值的函数。
void*的指针是存在的,也是4个字节,经常在一些函数的定义的参数上有应用到,比如memcpy,memmove。什么?你不认识,没事,这些关于内存的函数,你学久了就会认识的。
这里需要指出《C语言深度解剖》里的一个已经被时代更正的东西:
----------------------------------------------------我是图片---------------------------------------------------------------------------------------
----------------------------------------------------我是图片---------------------------------------------------------------------------------------
我在VS2013和linux下都尝试了一下,发现如果反过来,也就是:
... void *p1; int *p2; p2 = p1; ...
编译也能通过。当然你也可以自己试试,我的意思是,你必须得试一试。
八.讲一讲return。
return应该是最熟悉的关键字了,毕竟每个门函数都需要加一个“return 0;”
return可以返回的东西有很多,具体的值,静态变量。
这里请允许我再引用原书的一段内容。
----------------------------------------------------我是图片---------------------------------------------------------------------------------------
----------------------------------------------------我是图片---------------------------------------------------------------------------------------
现在也有用到一些,必须下面这个fun()函数:
int *fun(int *str) { int *ret=str; ... return ret; }
这里返回值为一个指向“栈内存”的内存,但是编译器也会让其通过,而且也没警告。
这里我有点不确定,希望你自己试一下。
九.volatile,register和const。
这三个放在一起也不是因为他们很像或者很极端只是有一个关于他们互相的实验。
先讲volatile:
volatile是类型修饰符,目的是为了告诉编译器这个变量可能会因为未知因素而改变,所以每次调用都要亲自去内存里调用之。换一句话说就是保证内存的可见性。
本来编译器会对代码进行优化,比如下面这个代码:
int i=10; int j = i;//(1)语句 int k = i;//(2)语句
这里代码会被优化,因为i的值没有改变,所以赋值给k的时候不必再去内存里取i的值。
这里我在linux环境下做了下面三个实验。
----------------------------------------------------我是图片---------------------------------------------------------------------------------------
----------------------------------------------------我是图片---------------------------------------------------------------------------------------
----------------------------------------------------我是图片---------------------------------------------------------------------------------------
----------------------------------------------------我是图片---------------------------------------------------------------------------------------
可以看到,三个代码,只有一点点不同,但是可以看到,运行程序的时间是register变量<一般局部变量
既然聊到这了,我们就来讲讲register:
register也是类型修饰符,说明这个变量需要放在寄存器里,什么意思呢?就是它会跑的更快,而且受到更好的保护,但是记得,一个CPU附近也就那么十几个寄存器,所以千万不要定义太多,到时候会适得其反。
还有点要注意,因为变量被放在寄存器里,所以无法用‘&’来取register变量的地址。
最后来讲讲const:
const的用途比较广泛,它可以用来修饰一般变量,修饰数组,修饰指针,也可以修饰函数,也经常用来修饰函数里的参数来起到保护参数不被改变的作用。
记住,const定义的变量只能在初始化时候定义,错过就真的是过错了!至于为什么,我们后面就讲到了。
你要是不信,我就拿出实锤:
C语言标准规定:在定义时就把它的内存空间给限制死了,要是不初始化,那块区域就永远是那样的。(有种车门焊死谁都不许下车的感觉...)
const定义变量后变量就成了只读变量,具有不可变性。但const修饰的值不能说成是常量,如果你用相同类型的指针指向其地址然后改其值,值依然会改变,但是会出现类型不匹配的警告。
int main() { const int a = 10; int *tmp = &a; *tmp = 20; printf("%d\n", a); system("pause"); return 0; }
输出结果是20。但同时也会出现如下警告:
----------------------------------------------------我是图片---------------------------------------------------------------------------------------
----------------------------------------------------我是图片---------------------------------------------------------------------------------------
最后写一个有代表性的语句:
问,下面语句表示的变量是什么意思?是否可以创建?
volatile const int i=10;
答:可以创建。这里考到了const类型的变量是在编译的时候创建了,因为是只读变量,在编译阶段编译器就告诉程序,这东西是只读的,所以这个东西的值就是这个(这也解释了上面‘只能在初始化时候定义’的具体原因)。而volatile类型变量是在编译和运行的时候,提醒编译器,“我这个变量不稳定,所以要经常来内存看看我”。这里就可以理解成——上面语句的意思是,这个变量在编译时为只读变量,不能修改且无法被优化,但是在运行时变量可以修改,因为不稳定,所以用到该变量时要不断去内存读取。
十.struct,union以及enum。
我们都知道,struct表示结构体,union表示联合体,enum表示枚举。
这些都不具体讲了,提几个重要的点:
I.struct里存在内存对齐。什么是内存对齐?说白了就是为了节约时间浪费空间——因为操作系统或者编译器调用内存中的值的时候不一定是一个一个字节读的,大多数都有一个跨度,也是为了效率。所以才有了内存对齐的必要,内存怎么对齐?记住这四点好了:
1. 第一个成员在与结构体变量偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS下为8 Linux下为4
3. 结构体总大小为最大对齐数(每个成员变量除了第一个成员都有一个对齐 数)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍 处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整 数倍。
(当然,默认对齐数可以用预编译指令"#pragma pack()"来改变,括号内必须是空或者2的自然数幂,当为空时表示取消默认对齐数。)
还有要记住,struct定义的时候由三部分组成:
struct tag { //tag是标签(1) member-list; //成员列表(2) }variable-list;//定义的变量列表(3)
以上部分中(1)可以省略,但是省略后无法在其他地方定义变量。(2)万万不可省略!所以必须要纠正书里的一些内容,书里对空结构体做了相应的分析,但现在空结构体完全不能编译通过,所以就不要再提了。(3)也可以省略,但是要记住,放在外部定义,和在函数内部定义,存放的位置是有差别的,如果定义的是一个数组,还要考虑到内存大小的一些问题,当然,不同情况不同应对方式,要好好锻炼自己,不要死背答案。
最后提一点,如果你有学C++会发现struct和class功能大致一致,struct的成员也可以是函数,唯一的区别就是class成员默认为private,union和struct成员默认为public。(如果还有别的区别希望能指出)
至于什么结构体数组,结构体指针,什么结构体内嵌套结构体时候的内存对齐情况,还有什么,拿代码试试,就都懂了,记住,多看书多实践!
II.union的特点就是成员公用同一片内存空间,而union变量的大小就是其最大成员的大小。
union这个特点有个很有用的实践应用——用来验证系统的大小端。
我们先说一下,什么是大小端?
大段:字数据的高字节存储在低地址中,而字数据的低字节则存放 在高地址中。
小段:字数据的高字节存储在高地址中,而字数据的低字节则存放 在低地址中。(小小小——小段小字节小地址)
看下面代码:
union un{ int i; char c; }Un_1; int main() { Un_1.i = 0x11223344; Un_1.c = 0x55; printf("%0x\n", Un_1); system("pause"); return 0; }
小段情况下,输出值为“11223355”,应该不难理解吧,同一片内存空间,int型4个字节,char只有一个。不解释——
III.enum变量只要记住它就可以看作是一个整型变量,所以其大小就是4。
需要提的就是,struct,union以及enum虽然说说是定义,但其实他们就是定义了一个类型,但也不能说是声明,因为他们真的创造了新的东西,所以最好的理解就是定义了一个新的类型,他们都发生在编译阶段。如果你不理解编译,我也很难用几句话教你,你就要知道一段代码的实现要经过:预处理——编译——汇编——链接的过程然后成为一个程序。你可以在linux环境下用gcc -E,gcc -s,gcc-c试一段代码。你可能会明白点,但其实我也只是略懂皮毛,还有很多要学。
十一.define和typedef。
typedef就像是给一个数据类型取一个别称,比如:
typedef struct student { int tmp; char c; }Stu_st;
就等于给结构体struct student 取了一个别称Stu_st,之后就可以用这个别称去定义变量。
那么可以考虑一个问题,看下面代码:
typedef struct student { int tmp; char c; }*Stu_pst; const Stu_pst stu3;//---------------(1) Stu_pst const stu4;//---------------(2) const int *tmp1;//-------------------(3) int* const tmp2;//-------------------(4)
我们给一个结构体指针定义了一个别称,现在问题来了。语句(1)和语句(2),意思一样吗?
答:是一样的。看到这个我们会联想到const分别修饰指针变量和指针指向的内容,就像语句(3)和语句(4),语句(3)表示指针指向的对象的内容不能改变,只要是他指向的内容就是一个只读变量,语句(4)表示指针的值,也就是地址不能改变。但是语句(1)和语句(2)不一样,他都表示const在修饰指针变量,也就是说指针的值不能够改变。为什么?
再答:其实可以这么想,站在编译器的角度,我是个编译器,我想确定const修饰的到底是什么变量,只要去掉其类型名即可,如果把语句(1)和语句(2)的类型名一去,不就是一样了吗,表示const在修饰指针变量stu3和stu4.
define不是关键字,它是个预编译指令。
为什么要把它拿出来呢?因为我喜欢啊~(开个玩笑)
define可以说和枚举很像,也和const有一定相似。
枚举比较#define定义,有类型检查,更加严谨,而且可以防止命名污染,便于调试,而且使用起来方便,一次可以定义多个变量。(都是网上大牛总结出来的结论)。
const比较#define定义。const发生在编译阶段,#define发生在预处理阶段。从汇编角度看,const只是给出了对应的内存地址,而#define定义给出了立即数,所以const定义的只读变量在运行时只有一份拷贝,而#define有多份拷贝。const修饰的只读变量具有类型,而#define定义没有。
得了,讲这么多,就是吹一下关键字,贬低一下#define。那你还真是误会我了。任何缺点换个角度,换个场景用,就是优点。再说,这篇微博讲的是关键字,下次有机会讲预处理命令,有的吹。
当然和typedef比较,#define也是有区别的,看下面代码:
#define INT32 int //---------(1) unsigned INT32 i = 10; typedef int int32; //---------(2) unsigned int32 j = 10
在上面情况下,(1)可以通过,(2)却不行。因为typedef不支持取得别名有类型扩展。
十一.break和continue。
这也没啥好讲的,前者直接退出循环,continue退出本次循环。
就提一个问题:
for(int i=0;i<10;i++) { if(i==2) continue; }
其实说白了就是continue之后会不会执行循环结束后的指令,也就是 i++ ?
答:会的。
十一.柔性数组。
用的不多,考的不少。
什么是柔性数组呢?
typedef struct st_type { int i; int a[]; //-------(1) }type_a;
上面的语句(1),就是柔性数组。他的特点有这么几个:
1.必须放在结构体最后,他可以说属于结构体,也可以说不属于结构体,因为在算结构体的内存空间的时候,并不算上它,可以说是挂名而已,不是正式成员。
2.如果你用malloc()分配给这个结构体类型的指针指向的柔性数组一块区域,再去求其指向内容的内存大小。不好意思,还是和原来一样。这也恰好验证了特点1。
3.柔性数组不可以单独放在结构体内。关于这个我做了一个实验,在linux下一切正常。但是!在VS2013下,如果一个结构体只有柔性数组,他定义的变量是有大小的,而且永远是4,而且是可以让程序跑起来的。真是奇怪啊,当我输入下面代码时:
typedef struct st_type { char a[]; }type_a;
type_a定义的变量大小依然是4,真是奇怪。大家就当无事发生吧,这玩意用的也太少了,当然你也可是试着玩玩嘿嘿嘿。
十二.总结。
最后说一下,学IT,写代码这东西,一定要好好培养兴趣,要一步一步来,慢慢来,多看书,多实践。
那些没有提的关键字,auto,signed,unsigned。都比较好理解。
要好好加油!希望将来能去携程!去上海!!!
然后,真心觉得博客和公众号差别蛮大的,可能因为公众号可以放歌,那我就在这里推荐一首歌:《一路逆风》——邓紫棋。再推荐一部电影:《实习生》——安妮·海瑟薇主演。
这一篇就这么结束吧,希望能保持这个好习惯。
感谢审阅。