转载出处:http://blog.csdn.net/mbh_1991/article/details/10149805
原文是一个系列文章,我将他们揉在了一起,并将原标题“C语言深度解剖读书笔记”改名为“C语言知识整理”,并将持续在原作者的基础上进行修改(修订)及增加。整理后文章如下
开始本节学习笔记之前,先说几句题外话。其实对于C语言深度解剖这本书来说,看完了有一段时间了,一直没有时间来写这篇博客。正巧还刚刚看完了国嵌唐老师的C语言视频,觉得两者是异曲同工,所以就把两者一起记录下来。等更新完这七章的学习笔记,再打算粗略的看看剩下的一些C语言的书籍。
第一节 关键字使用
- #include <stdio.h>
- #include <stdlib.h>
- int main(void)
- {
- static int j=0;
- int k;
- void fun1()
- {
- j=0;
- j++;
- printf("fun1 %d\n",j);
- }
- void fun2()
- {
- static int i=0;
- //i=0;
- printf("fun2 %d\n",i);
- i++;
- }
- for(k=0;k<10;k++)
- {
- fun1();
- fun2();
- }
- return 1;
- }
- #include <stdio.h>
- #include <stdlib.h>
- void fun(int b[100])
- {
- printf("sizeof(b) is %d\n",sizeof(b));
- }
- int main(void)
- {
- int *p=NULL;
- int a[100];
- int b[100];
- printf("sizeof(p) is %d\n",sizeof(p));
- printf("sizeof(*p) is %d\n",sizeof(*p));
- printf("sizeof(a[100]) is %d\n",sizeof(a[100]));
- printf("sizeof(a) is %d\n",sizeof(a));
- printf("sizeof(&a) is %d\n",sizeof(&a));
- printf("sizeof(&a[0] is %d\n",sizeof(&a[0]));
- fun(b);
- return 1;
- }
a.对于bool类型的比较:FLASE都是0 TRUE不一定是1 所以应该用if(bool_num); if(!bool_num);
- cosnt int* func()
- {
- static int count = 0;
- count++;
- return &count;
- }
h. 在看const修饰谁,谁不变的问题上,可以把类型去掉再看,代码如下:
- struct student
- {
- }*str;
- const str stu3;
- str const stu4;
str是一个类型 ,所以在去掉类型的时候,应该都变成const stu3和const stu4了,所以说应该是stu4和stu3这个指针不能被赋值。
12.关键字volatile:
volatile搞嵌入式的,一定都特别属性这个关键字,记得第一使用这个关键字的时候是在韦东山老师的,Arm裸机视频的时候。volatile是告诉编译不要对这个变量进行任何优化,直接在内存中进行取值。一般用在对寄存器进行赋值的时候,或修饰可能被多个线程访问的变量。
- #include <stdio.h>
- #include <stdlib.h>
- int main(void)
- {
- int a[5]={1,2,3,4,5};
- int *p=(int *)(&a+1); //数组指针 加一 进行正常的指针运算 走到数
- 组尾
- int *d=(int *)((int)a+1);//地址加一 不是指针运算
- //printf("%x\n",*((char *)((int)a+1)-1));
- /*因为是小端存储 高地址 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x01 低地址*/
- /*变成了 0x02 0x00 0x00 0x00 */
- printf("%x,%x",p[-1],*d); /* 第二个值就是这么存储的0x02 0x00 0x00 0x00 低地址处 所以就是2000000*/
- int a=0x11223344;
- char *p=(char *)((int)&a);
- printf("%x\n%x\n",*(p+0),p+0);
- printf("%x\n%x\n",*(p+1),p+1);
- return 0;
- }
- #include <stdio.h>
- #include <stdlib.h>
- union
- {
- int i;
- char a[2];
- }*p,u;
- int main(void)
- {
- p=&u;
- p->i=0x3839;
- printf("%x\n",p->i);
- printf("a0p=%x,a1p=%x\n",&(p->a[0]),&(p->a[1]));
- printf("a0=%x,a1=%x\n",p->a[0],p->a[1]);
- return 0;
- }
枚举enum其实就是 int类型,用来保存枚举常量的。enum枚举类型,这个才是真正的常量,定义常量一般用enum 。#define是宏定义是在预编译期间单纯的替换。#define宏定义无法调试,枚举常量是可以调试的。#define宏定义是无类型信息的,枚举类型是有类型信息的常量,是int型的。
a.typedef用于给一个已经存在的数据类型重新命名。
- typedef unsigned int int32;
typedef char* PCHAR; PCHAR p1,p2; //p1和p2都是 char*型
e.有一个知识点忘记了,嘿嘿,程序如下:
- typedef struct student
- {
- }str,*str1;
str1 abc; 就是定义一个struct student *类型
str abc; 就是定义一个struct student 类型
- 程序一:
- for(i=0; i<m; i++)
- {
- for(j=0; j<n; j++)
- {
- for(k=0; k<p; k++)
- {
- c[i][j] = a[i][k] * b[k][j];
- }
- }
- }
- 程序二:
- for(i=0; i<m; i++)
- {
- for(k=0; k<p; k++)
- {
- for(j=0; j<n; j++)
- {
- c[i][j] = a[i][k] * b[k][j];
- }
- }
- }
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- int main()
- {
- char a[1000];
- int i;
- for(i=0; i<1000; i++)
- {
- a[i] = (-1-i);
- }
- while(a[i])
- {
- printf("%d\n",a[i]);
- i++;
- }
- printf("%d\n",strlen(a));
- return 0;
- }
第二节 字符技巧
1.注释问题:
注释不能把关键字弄断,如:in/*注释*/t
注释不是简单的剔除,而是使用空格替换
编译器认为双引号括起来的内容都是字符串,双斜杠也不例外。如:char *p = "heh//jfeafe" //不起注释作用
2.接续符:
接续符\ ,常用于宏定义中
- #define SWAP(a,b) \
- { \
- int temp = a; \
- a = b; \
- b = temp; \
- }
反斜杠同时有接续符和转义符两个用途,当接续符使用的时候,可以直接在程序中出现。当转义符使用的时候,必须是出现在字符串中。
接续符,也用与接续一个关键字,代码如下, 注意: 直接连接\两边不能有空格。
- #include <stdio.h>
- #include <stdlib.h>
- int main()
- {
- cha\
- r a = 12;
- return 0;
- }
3. 逻辑运算符:短路规则
4. 最容易忘记规则的两个运算符
三目运算符:(a?b:c) 当a的值为真的时候 返回b的值,否则返回c的值
逗号表达式:a,b 表达式的值为b的值
5. 位运算
对于左移和右移<< >>问题 :无符号的,和有符号左移,都是补0 ,对于有符号的在右移动的时候,正数补零,负数补什么跟编译器有关系。并且左移和右移的大小不能大于数据的长度,也不能小于0。
交换两个数,有一种不借助中间变量的方法,就是异或,代码如下:
- #include <stdio.h>
- #define SWAP1(a,b) \
- { \
- int temp = a; \
- a = b; \
- b = temp; \
- }
- #define SWAP2(a,b) \
- { \
- a = a + b; \
- b = a - b; \
- a = a - b; \
- }
- #define SWAP3(a,b) \
- { \
- a = a ^ b; \
- b = a ^ b; \
- a = a ^ b; \
- }
- int main()
- {
- int a = 1;
- int b = 2;
- SWAP1(a,b);
- SWAP2(a,b);
- SWAP3(a,b);
- return 0;
- }
6. ++与--
顺序点就是什么时候开始加,什么时候开始减。
i = 0;
printf("%i", i++); /* i=0; printf(i); i=i+1; */
printf("%i", ++i): /* i=i+1; printf(i) */
7. 贪心法
每一个符号应该尽可能多的包含字符
8. 符号运算优先级问题
个人觉得优先级不用记,好好的写括号吧~~~
给一个易错优先级表,如图:
9.c语言中的类型转换
c语言中有两种转换类型,分别是:隐式转换和显示转换(强制类型转换)
隐式转换的规则:
a.算术运算中,低类型转换为高类型
b.赋值运算中,表达式的类型转换为左边变量的类型
c.函数调用时,实参转换成形参的类型
d.函数返回值,return表达式转换为返回值的类型
隐式转换的例子,代码如下:
- #include <stdio.h>
- int main()
- {
- int i = -2;
- unsigned int j = 1;
- if( (i + j) >= 0 )
- {
- printf("i+j>=0\n");
- }
- else
- {
- printf("i+j<0\n");
- }
- printf("i+j=%d\n", i + j);
- return 0;
- }
注意:在使用C语言的时候,应该特别注意数据的类型是否相同,尽量避免隐式转换带来的不必要的麻烦~~~
第二节 预编译处理
1. 编译过程的简介
C和C++文件的编译是以*.c或*.cpp为单位进行编译的!
预编译:
a.处理所有的注释,以空格代替。
b.将所以#define删除,并展开所有的宏定义,字符串替换。
c.处理条件编译指令#if,#ifdef,#elif,#else,#endif
d.处理#include,并展开被包含的文件,把头文件中的声明,全部拷贝到文件中。
e.保留编译器需要使用的#pragma指令、
怎么样观察这些变化呢?最好的方法就是在GCC中,输入预处理指令,可以看看不同文件经过预处理后变成什么样了,预处理指令:gcc -E file.c -o file.i 注意:-C -E一起使用是预编译的时候保留注释。
编译:
a.对预处理文件进行一系列词法分析,语法分析和语义分析
词法分析:主要分析关键字,标示符,立即数等是否合法
语法分析:主要分析表达式是否遵循语法规则
语义分析:在语法分析的基础上进一步分析表达式是否合法
b.分析结束后进行代码优化生成相应的汇编代码文件 编译指令:gcc -S file.c -o file.s
汇编:
汇编器将汇编代码转变为机器可以执行的指令,每个汇编语句几乎都对应一条机器指令,其实机器指令就是机器码,就是2进制码。汇编指令:gcc -c file.c -o file.o 注意:-c是编译汇编不连接。
链接:
再把产生的.o文件,进行链接就可以生成可执行文件。连接指令:gcc file.o file1.o -o file 这句指令是链接file.o和file1.o两个编译并汇编的文件,并生成可执行文件file。
链接分两种:静态链接和动态链接,静态链接是在编译器完成的,动态链接是在运行期完成的。静态链接的指令是:gcc -static file.c -o file对于一些没有动态库的嵌入式系统,这是常用的。
一般要想通过一条指令生成可执行文件的指令是: gcc file.c -o file
2. c语言中的预处理指令
#define、#undef(撤销已定义过的宏名)、#include、#if、#else、#elif、#endif、#ifdef、#ifndef、#line、#error、#pragma。还有一些ANSI标准C定义的宏:__LINE__、__FILE__、__DATA__、__TIME__、__STDC__。这样使用printf("%s\n",__TIME__); printf(__DATE__);
一个#undef的例子:
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #define X 2
- #define Y X*2
- #undef X
- #define X 3
- int main()
- {
- printf("%d\n",Y);
- return 0;
- }
这个输出的是6,说明了#undef的作用
3. 宏定义字符串的时候
应该是 #define HELLO "hello world" 记住是双引号。还有就是一切宏都是不能有分号的,这个一定要切忌!!!
4. 宏与函数的比较
a.宏表达式在预编译期被处理,编译器不知道有宏表达式存在
b.宏表达式没有任何的"调用"开销
c.宏表达式中不能出现递归定义
5. 为什么不在头文件中定义全局变量
如果一个全局变量,想要在两个文件中,同时使用,那这两个文件中都应该#include这个头文件,这样的话就会出现重复定义的问题。其实是重名的问题,因为#include是分别在两个文件中展开的,试想一下,如果在两个文件中的开始部分,都写上int a = 10; 是不是也会报错。可能你会说那个#ifndef不是防止重复定义吗?是的 ,那是防止在同一个文件中,同时出现两次这个头文件。现在是两个文件中,所以都要展开的。全局变量就重名了!!!所以 对于全局变量,最好是定义在.c文件中,不要定义在头文件中。
6.#pargma pack 设置字符对齐
详细参考我的另一篇博文小侃#pragma
7.#运算符(转换成字符串):
假如你希望在字符串中包含宏参数,那我们就用#号,它把语言符号转换成字符串。
#define SQR(x) printf("the "#x"lait %d\n",((x)*(x)));
SQR(8)
输出结果是:the 8 lait 64 这个#号必须使用在带参宏中
有个小例子:
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- /*在字符串中 加入宏参用的*/
- #define SCAN(N,String) scanf("%"#N"s",String); //N是截取的个数 String是存储的字符串
- int main()
- {
- char dd[256];
- SCAN(3,dd) //记得没有分号哈 自定义 任意格式输入的scanf 截取输入的前三个
- printf("%s\n",dd);
- return 1;
- }
8. ##运算符(粘合剂)
一般用于粘贴两个东西,一般是用作在给变量或函数命名的时候使用。如#define XNAME(n) x##n
XNAME(8)为8n 这个##号可以使用在带参宏或无参宏中
下面是一个##运算符的小例子,代码如下:
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #define BL1 bb##ll##1
- #define BL(N) bbll##N
- int main()
- {
- int BL1=10;
- int BL(4)=15;
- printf("%d\n",bbll1);
- printf("%d\n",bbll4);
- return 1;
- }
注意:#号和##号都必须只能在宏定义中使用,不能使用在其他地方
9. 不常用到的预编译指令
用到的时候查查就好。比如说#line、#error、#warning等。
第三节 结构体内存对齐
1. 结构体为什么要内存对齐(也叫字节对齐)
其实我们都知道,结构体只是一些数据的集合,它本身什么都没有。我们所谓的结构体地址,其实就是结构体第一个元素的地址。这样,如果结构体各个元素之间不存在内存对齐问题,他们都挨着排放的。对于32位机,32位编译器(这是目前常见的环境,其他环境也会有内存对齐问题),就很可能操作一个问题,就是当你想要去访问结构体中的一个数据的时候,需要你操作两次数据总线,因为这个数据卡在中间,如图:
在上图中,对于第2个short数据进行访问的时候,在32位机器上就要操作两次数据总线。这样会非常影响数据读写的效率,所以就引入了内存对齐的问题。
另外一层不太重要的原因是:某些硬件平台只能从规定的地址处取某些特定类型的数据,否则会抛出硬件异常。
2. 内存对齐的规则
a.第一个成员起始于0偏移处
b.每个成员按其类型大小和指定对齐参数n中较小的一个进行对齐
c.结构体总长度必须为所有对齐参数的整数倍
d.对于数组,可以拆开看做n个数组元素
3. 来几个小例子,画画图,有助于理解
第一个例子,代码如下:
- #include <stdio.h>
- struct _tag_str1
- {
- char a;
- int b;
- short c;
- }str1;
- struct _tag_str2
- {
- char a;
- short c;
- int b;
- }str2;
- int main()
- {
- printf("sizeof str1 %d\n",sizeof(str1));
- printf("sizeof str2 %d\n",sizeof(str2));
- return 0;
- }
看图很自然就知道了str1为12个字节,str2为8个字节。
第二个例子,上面的那个例子有好多问题还没有考虑到,比如说上面的那个例子在8字节对齐,和4字节对齐的情况都是一样的。结构体中嵌套结构体的内存对齐怎么算,所以就有了这个例子,代码如下:
- #include <stdio.h>
- #pragma pack(8)
- //#pragma pack(4)
- struct S1
- {
- short a;
- long b;
- };
- struct S2
- {
- char c;
- struct S1 d;
- double e;
- };
- #pragma pack()
- int main()
- {
- struct S2 s2;
- printf("%d\n", sizeof(struct S1));
- printf("%d\n", sizeof(struct S2));
- printf("%d\n", (int)&(s2.d) - (int)&(s2.c));
- return 0;
- }
在4字节对齐的情况中,有一个问题值得注意:就是图中画1的地方。这里面本应short是可以上去的。但是对于结构体中的结构体一定要十分警惕,S1是一体的,short已经由于long进行了内存对齐,后面还空了两个字节的内存,其实此时的short已经变成了4个字节了!!!即结构体不可拆,不管是多少字节对齐,他们都是一体的。所有的圈都变成了叉。所以说结构体只能往前篡位置,不能改变整体。
我们在分析一些8字节对齐的情况,如图:
同样,到这里又有一个字节对齐的原则要好好重申一下:就是以什么为对齐参数,首先我们要知道编译器或者自己定义的是多少字节对齐的,这个数为n。然后我们要看这个结构体中的各个数据类型,找到所占字节数最大的类型,为m。如果n大于m,就以m为对齐参数,比如说一个4字节对齐的结构体中都是short,那这个结构体以什么为对齐参数,当然是2了,如果m大于n,就以n为对齐参数,比如说在4字节对齐的情况下的double类型。
以上就是我对内存对齐的小总结,最最想要说明的就是两大段红色的部分。
第四节 堆栈问题
- #include <stdio.h>
- void fun()
- {
- int a;
- int b;
- int c;
- printf("funa %p\n",&a);
- printf("funb %p\n",&b);
- printf("func %p\n",&c);
- }
- void main()
- {
- int a;
- int b;
- int c;
- int d;
- int e;
- int f;
- int p[100];
- printf("a %p\n",&a);
- printf("b %p\n",&b);
- printf("c %p\n",&c);
- printf("d %p\n",&d);
- printf("e %p\n",&e);
- printf("f %p\n",&f);
- printf("p0 %p\n",&p[0]);
- printf("p1 %p\n",&p[1]);
- printf("p2 %p\n",&p[2]);
- printf("p3 %p\n",&p[3]);
- printf("p4 %p\n",&p[4]);
- printf("p10 %p\n",&p[10]);
- printf("p20 %p\n",&p[20]);
- printf("p30 %p\n",&p[30]);
- printf("p80 %p\n",&p[80]);
- printf("p90 %p\n",&p[90]);
- printf("p100 %p\n",&p[100]);
- fun();
- }
通过上面的运行结果,可以分析得出:在同一个函数中, 先定义的变量在高地址处,后定义的变量在低地址处,且他们的地址是相连的中间没有空隙。 定义的数组是下标大的在高地址处,下标小的在低地址处(由此可以推断出malloc开辟出的推空间,也应该是下标大的在高地址处,下标小的在低地址处)。 子函数中的变量,跟父函数中的变量的地址之间有很大的一块空间,这块空间应该是两个函数的其他活动记录,且父函数中变量在高地址处,子函数中的变量在低地址处。
- #include<stdio.h>
- /*这是一个死循环*/
- /*这里面有数组越界的问题*/
- /*有栈空间分配的问题*/
- int main()
- {
- int i;
- // int c;
- int a[5];
- int c;
- printf("i %p,a[5] %p\n",&i,&a[5]); //观察栈空间是怎么分配的 这跟编译器有关系的
- printf("c %p,a[0] %p\n",&c,&a[0]);
- for(i=0;i<=5;i++)
- {
- a[i]=-i;
- printf("%d,%d",a[i],i);
- }
- return 1;
- }
l. 最后说说数据结构中的栈,其实数据结构中的栈就是一个线性表,且这个线性表只有一个入口和出口叫做栈顶,还是LIFO(后进先出的)结构而已。