笔记内容来自本人学习 狄泰软件学院 唐佐林 老师的视频,相关课件截图已授权
本文章未经许可不允许以任何方式转载和复制保存
C第六课
if语句分析
1.bool型变量应直接出现与条件中,不要进行比较
2.变量和常量值比较时,常量值应该出现在比较符合左边
3.float型变量不能直接进行0值比较,需要定义精度
示例程序:
int i = 1;
if( 0 == i ){}
#define EPSINON 0.00000001
float f = 0.0;
if( (-EPSINON <= f) && (f <= EPSINON) ){}
对于第二点,如果常量值放在==左边,当我们漏写了一个=的时候,编译器就会帮我们报错检查出错误,常量值不能出现在=的左边;对于第三点,因为浮点型是不精确的,即使初始化为0.0,但在内存中可能是0.00000001,所以float型变量与0比较可能结果都是不等于0的
switch语句分析
1.case语句分支必须要有break,否则会导致分支重叠
2.default语句有必要加上,以处理特殊情况
3.case语句和switch语句中的值只能是整型常量或字符型常量,不能是浮点型
示例程序:
int i = 0;
switch( i )
{
case 1:/*...*/break;
case 2.0:/*...*/break;//error,2.0不是整型或字符型常量
default:/*...*/break;
}
如果case 1后没有break,如果case 1成立的话,直接跳过case 2的判断,而直接执行case 2中对应的代码块,如果case 2也没有加break,那就继续向下执行,以此类推
C第十四课
单引号和双引号
1.C语言中的单引号用来表示字符字面量,代表一个整数
如’a’表示字符字面量,在内存中占1个字节,’a’+1表示’a’的ASCII码值+1,结果为’b’
2.”a”表示字符串字面量,代表字符指针,在内存中占2个字节,”a”+1表示指针运算,结果指向”a”结束符’\0’
3.C编译器接受字符和字符串的比较,但无任何意义
4.C编译器允许字符串对字符变量赋值,但会发生截断,得到错误结果
示例程序:
#include <stdio.h>
int main()
{
char* p1 = 1;
char* p2 = ‘1’;
char* p3 = “1”;
printf("%s,%s,%s",p1,p2,p3);
printf(‘\n’);
return 0;
}
以上程序编译通过,但会有警告。在运行时发生段错误。原因:p1指向地址0x00000001,p2指向地址0x00000031(‘1’的ASCII码对应的十六进制为31),printf在解析的时候,将前两个参数解析为上述两个地址,但是,内存的低地址空间不能在程序中随意访问,分界点为0x08048000,低于它的都为低地址。’\n’对应的地址为0x00000010,也会段错误
C第十六课
运算优先级:四则运算>位运算>逻辑运算
C第二十七课
1.数组可以这样将所有元素初始化为0:int a[5] = {0};第一个元素被赋值为0,剩余的元素由于没有手动赋值,编译器自动初始化为0
2.
3.数组名代表数组首元素的地址
4.数组的地址需要用取地址符&才能得到
5.数组首元素的地址值与数组的地址值相同,但意义不同
6.因地址是指针,上述两个地址值意义不同是因为指针的步长不同
如:
int a[5] = {0};
a的值跟&a的值相同,但是a+1得到的是a的地址向后移动1sizeof(首元素)=1sizeof(int)=4个字节那么长,而&a+1得到的是a的地址向后移动1sizeof(a)=1sizeof(int)*5=20个字节那么长
数组名的盲点
1.数组名有时候可以看做一个常量指针,但不可以把数组名等同于指针,sizeof任何一种指针结果都是4
2.数组名不包含数组的长度信息,只包含数组的起始地址和数组首元素的长度信息
3.在表达式中数组名只能做为右值使用,对应第二点
4.只有在下列场合中数组名不能看做常量指针
(1)数组名做为sizeof操作符的参数
(2)数组名做为&运算符的参数
在以上两种情况下,编译器会获取这个数字的结构、类型信息,从而得到其长度
C第二十八课
指针和数组分析
1.指针之间只支持减法运算
2.参与减法运算的指针类型必须相同,即指向同一个数组,得到下标差,这样才有意义
3.指针也可以进行关系运算(<, <=, >, >=)
4.指针关系运算的前提是同时指向同一个数组中的元素
5.任意两个指针之间的比较运算(==,!=)无限制
6.参与关系运算的指针类型必须相同,否则无意义
示例程序:
#include <stdio.h>
#define DIM(a) (sizeof(a)/sizeof(*a))
int main()
{
char s[] = {‘h’,’e’,’l’,’l’,’o’};
char* pBegin = s;
char* pEnd = s + DIM(s);
for(p = pBegin;p<pEnd;p++)
{
printf(“%c”,*p);
}
return 0;
}
宏定义函数DIM用来求一个数组的元素个数。pEnd指向了元素’0’的后面一个位置,在C语言里,这个位置是合法的,是个擦边球,也是个技巧,在C++的STL里常用到
C第二十九课
指针和数组分析(二)
既然数组名有时候可以当作常量指针,那么指针可不可以当作数组名来使用呢?
答:可以!
下标形式VS指针形式
a[n] <-> *(a+n) <-> *(n+a) <-> n[a]
PS:指针形式效率比下标形式高,但代码可读性和代码维护性不如下标形式
示例程序:
(一)
int a[5] = {0};
int* p = a;
for(i=0;i<5;i++)
{
p[i] = i+1;//指针当数组名使用
}
for(i=0;i<5;i++)
{
i[a] = i+10;//上述公式的转换,本语句是合法的,相当于a[i] = i + 10;
}
验证指针和数组名不完全等同
在ext.c文件中:
int a[5] = {1,2,3,4,5};
在test.c文件中:
#include <stdio.h>
int main()
{
extern int a[5];
printf(“&a = %p\n”,&a); //&a = 0x08049999(假设是这个地址)
printf(“a = %p\n”,a); //a = 0x08049999
printf(“*a = %d\n”,*a); //*a = 1
/*以上结果毫无疑问,因为整个数组的地址跟数组的首元素的地址相同*/
/*但如果把代码改成如下:*/
extern int* a;
printf(“&a = %p\n”,&a); //&a = 0x08049999(假设是这个地址)
printf(“a = %p\n”,a); //a = 0x1
printf(“*a = %d\n”,*a); //段错误
return 0;
}
为什么把ext.c文件中的数组a在tes.c文件中声明为指针后,结果完全不同?
答:首先为什么第一个打印出来的地址值跟前面的代码一样?因为a这个标识符已经在ext.c文件中存在了,是一个数组,即有了一段内存,编译器直接将test.c文件中声明的指针a映射要该段内存。
为什么后面两个打印的跟第一种情况不一样呢?
答:因为a现在是一个指针,大小为四个字节,当执行第二个printf函数的时候因为要取a的值,所以编译器去a对应的那个内存去取四个字节出来,即取出了ext.c文件里的那个数组的第一个元素1,所以结果0x1;因为0x1是低地址,不能随便被程序访问,所以段错误
所以,指针和数组名不完全等同!
a和&a的区别
示例程序
int a[5] = {1,2,3,4,5};
int* p1 = (int*)(&a+1);
int* p2 = (int*)((int)a+1);
int* p3 = (int*)(a+1);
printf(“%d,%d,%d\n”,p1[-1],p2[0],p3[1]); //5,乱码,3
a代表首元素的地址,步长为一个元素的长度;&a是整个数组的地址,步长为整个数组的长度;p1指向5后面的那个地址,下标为-1则向前退一个int大小的长度,即a[4]的起始地址,所以打印出5;在p2中,对a的地址强制转换为十进制,再+1,再强制转换为int*,即变为了十六进制表示的地址,相比于原来a的地址,仅仅向后移动了1个字节,所以打印出乱码(根据系统的大小端和a的地址可以将此乱码计算出来);p3容易理解
函数中有数组做为参数
1.void f(int a[]); <-> void f(int* a);
2.void f(int a[5]); <-> void f(int* a);
结论:数组做为函数参数时,它的长度信息不会传递过去,退化为指针,所以当定义的函数中有数组参数时,需要定义另一个参数来标识数组的大小
示例程序:
void fun1(char a[5])
{
printf(“sizeof(a) = %d\n”,sizeof(a));//4
a = NULL;//合法
}
a退化为指针,指针大小为4;a=NULL合法说明a不是数组名而是指针,可以做为左值
C第三十课
字符数组与字符串
1.在C语言中,双引号引用的单个或多个字符是一种特殊的字面量
-存储与程序的全局只读存储区
-本质为字符数组,编译器自动在结尾加上'\0'字符
如下语句均为合法的:
(1)char ca[] = {‘H’,’e’,’l’,’l’,’o’};
(2)char sa[] = {‘H’,’e’,’l’,’l’,’o’,’\0’};
(3)char ss[] = “Hello world!”;
(4)char* str = “Hello world!”;
鲜为人知的小秘密
1.字符串字面量的本质是一个数组
2.字符串字面量可以看作常量指针
3.字符串字面量中的字符不可改变,因为存储与只读存储区
4.字符串字面量至少包括一个字符,当双引号中为空时,仍然有一个’\0’
字符串字面量
“Hello World!”是一个无名的字符数组
如下表达式均正确:
1.char b = “abc”[0]; //b的值是’a’
2.char c = (“123”+1); //c的值是’2’,”123”是常量指针
3.char t = ””; //t的值是’\0’
字符串的长度
1.字符串的长度指的是第一个’\0’字符前出现的字符个数
2.通过’\0’结束符来确定字符串的长度
3.函数strlen用于返回字符串的长度
PS:因字符串是一个字符数组,所以sizeof一个字符串即相当于求一个数组的长度,如果该字符串中有多个’\0’,此时不会因遇到’\0’而结束测量数组长度,这个strlen函数不同,strlen函数遇到第一个’\0’就结束,立刻返回长度
如:
printf(“%d\n”,sizeof(“123\0abc\0”)); //打印9,因为虽然最后的字符是’\0’,但是编译器都会自动再加上一个’\0’
printf(“%d\n”,strlen(“123\0abc\0”)); //打印3
C第三十一课
关于snprintf函数
-snprintf函数本身是可变参数函数,原型如下:
int snprintf(char* buffer,int buf_size,const char* fomart,…);
当函数只有3个参数时,如果第三个参数没有包含格式化信息(比如%s,%d等),函数调用没有问题;相反,如果第三个参数包含了格式化信息,但缺少后续对应参数,则程序行为不确定。
PS:格式化信息必须与变参个数相匹配
如:
#define STR1 “Hello,\0Guagua\0”
#define STR2 “Hello,\0%sChengzi\0”
char* src1 = STR1;
char buf1[255] = {0};
char* src2 = STR2;
char buf2[255] = {0};
snprintf(buf1,sizeof(buf1),src1);
snprintf(buf2,sizeof(buf2),src2);
snprintf函数跟printf函数类似,printf函数是把参数输出到显示器上,只不过snprintf函数是把第三个参数输出到第一个参数
STR2中有格式化信息%s,而第二个snprintf函数中只有三个参数,这样运行后结果不确定,应改为snprintf(buf2,sizeof(buf2),src2,”I Love You ”);则把”Hello,\0I Love You Chengzi\0”输出到buf2中
典型问题
#define S1 “Guagua and Chengzi”
#define S2 “Guagua and Chengzi”
if(S1 == S2)
{
printf(“Equal\n”);
}
else
{
printf(“Not Equal\n”);
}
不同编译器的处理结果不同,有点编译器对S1,S2做了优化,认为这两个字符串的值一样,而””操作符在这个时候比较的是S1和S2的地址,由于做了优化,所以S1S2;而有的编译器不会优化,分别给S1和S2都分配了不同地址,所以S1!=S2
结论
1.字符串之间的相等比较需要用strcmp完成,不可以直接用==进行字符串之间的比较
2.不编写依赖特殊编译器的代码
C第三十二课
数组类型
-C语言中的数组有自己特定的类型
-数组的类型由元素类型和数组大小共同决定
例:int array[5]的类型为int [5]
定义数组类型
-C语言中通过typedef为数组类型重命名
typedef type(name)[size];
例:typedef int(INT5)[5]; INT5 iarray;
数组指针
1.数组指针用于指向一个数组,而不是数组首元素
2.数组名是数组首元素的起始地址,但并不是数组的起始地址
3.通过将取地址符&作用于数组名可以得到数组的起始地址
4.可以通过数组类型定义数组指针:ArrayType* pointer
5.也可以直接定义:type(*pointer)[n];
示例程序:
typedef int(INT5)[5];
typedef float(FLOAT10)[10];
typedef char(CHAR9)[9];
INT5 a1;
float fArray[10];
FLOAT10* pf = &farray;//合法
CHAR9 cArray;
char(*pc)[9] = &cArray;//合法
char(*pcw)[4] = cArray;//error
printf(“%d,%d\n”,sizeof(INT5),sizeof(a1));//20,20
cArray是个数组名,代表首元素的地址,pcw是一个数组的指针,两者指针类型不同,不能赋值,编译出错
指针数组
本质还是数组,只不过元素类型为指针
示例程序:
#define DIM(a) (sizeof(a)/sizeof(*a)) //求数组长度
int lookup_keyword(const char* key,const char* table[],const int size)
{
int ret = -1;
for(i=0;i<size;i++)
{
if(strcmp(key,table[i]) == 0)
{
ret = i;
break;
}
}
return ret;
}
const char* keyword[] = {“Guagua”,”Chengzi”};
/*keyword实质是指针数组,每个元素是个指针,指向一个字符串常量*/
printf(“%d\n”,lookup_keyword(“Chengzi”,keyword,DIM(keyword))); //1
C第三十六课
函数类型
1.C语言中的函数有自己特定的类型
2.函数的类型由返回值,参数类型,参数个数共同决定,如int add(int i,int j)的类型为int(int,int)
3.C语言中通过typedef为函数类型重命名
typedef type name(parameter list),如typedef int f(int,int);
函数指针
1.函数指针用于指向一个函数
2.函数名是执行函数体的入口地址
3.对函数名取地址,得到的还是函数的入口地址(要与数组名区分)
4.可通过函数类型定义函数指针:FuncType* pointer;
5.也可以直接定义:type (*pointer)(parameter list);
如何使用C语言直接跳转到某个固定的地址开始执行?
答:通过函数指针即可
示例程序:
typedef int(FUNC)(int);
int test(int i){return i*i;}
void f(){}
int main()
{
FUNC* pt = test;
void(*pf)() = &f;
printf(“pf = %p\n”,pf); //0x08048400
printf(“f = %p\n”,f); //0x08048400
printf(“&f = %p\n”,&f); //0x08048400
pf();
(*pf)();
/*上面两种调用方法都合法,第一种比较常用*/
return 0;
}
回调函数
1.回调函数是利用函数指针实现的一种调用机制
2.回调机制中的调用者和被调函数互不依赖
示例程序:
typedef int(*Weapon)(int);
void fight(Weapon wp,int arg)
{
int result = 0;
result = wp(arg);
printf(“Boss loss:%d\n”,result);
}
int knife(int n)
{
return n*5;
}
int sword(int n)
{
return n*10;
}
fight(knife,3);
fight(sword,5);
C第三十九课
函数调用过程
1.程序中的函数用栈来维护其内存(即活动记录),每次函数调用都对应着一个栈上的活动记录。
(1)调用函数的活动记录位于栈的中部
(2)被调函数的记录位于栈的顶部
如:
esp指针指向栈顶地址,ebp指向函数调用结束之后的返回地址
当main函数调用f函数的时候,ebp若向后回退四个字节,就指向了返回地址,即回到了esp指针上一次指向的位置,esp回退到那里;ebp若向前进四个字节,就指向了ebp上一次指向的位置,ebp回退到那里,恢复到从main函数开始运行的那个样子,然后从main函数开始继续执行下一个函数,即加载下一个函数的活动记录。
函数的返回只是修改ebp指针和esp指针的地址值,栈空间里的数据不会因为函数的返回而立即改变。
所以,为什么不能返回函数里局部变量或者局部数组呢?
因为,比如上图,在f函数返回后,虽然它以前的活动记录即那段内存空间数据没变,但是当main函数继续向下执行另一个函数的时候,栈上以前存放f函数记录的地方会被用来存放另一个函数的活动记录,所以此时返回f函数的局部数据是没有意义的,会发生错误。
示例程序:
#include <stdio.h>
int* g()
{
int a[10] = {0};
return a;
}
void f()
{
int i = 0;
int* pointer = g();
for(i=0;i<10;i++)
{
printf(“%d\n”,pointer[i]);
}
}
int main()
{
f();
return 0;
}
在f函数里打印的不全是10个0,因为在g函数返回后,下面将执行printf函数,g函数的活动记录内存段被printf函数覆盖,g函数原来的数据不再有效
程序中的堆
当系统用空闲链表法管理堆内存时,我们调用malloc函数时可能会返回大于我们所需的字节数,如上图所示。
程序中的静态存储区
1.静态存储区随着程序的运行而分配空间
2.静态存储区的生命周期直到程序运行结束
3.在程序的编译期静态存储区的大小就已经确定,程序运行时不允许静态存储区被改变
4.静态存储区主要用于保存全局变量和静态局部变量
5.静态存储区的信息最终会保存到可执行程序中
C第四十课
程序文件的一般布局
答案:是
注意
bss区的变量都默认初始化为0,而堆、栈上的变量如果没有显示的初始化,则为随机值,即使输出是0也是个巧合
C第四十一课
野指针
野指针的由来
基本原则
C第四十二课
常见内存错误
1.结构体里有指针成员,指针成员未初始化
如:struct Demo{char* p;}; struct Demo d1; d1.p[i] = 0;//段错误
2.结构体成员指针未分配足够的内存空间
如:char* p = (char*)malloc(5); strcpy(p,”123456”);//p只申请了5个字节,不足够容纳”123456”,造成内存越界
3.内存分配成功,但未初始化
如:char* p1 = (char*)malloc(5); char* p2 = (char*)malloc(5); strcpy(p1,p2);//指针会越界,因为p2没有初始化字符串,即没有’\0’结束符,那就会从p2所保存的地址一直读下去,造成内存操作越界
4.内存操作越界
PS:内存操作越界不一定会段错误。因为当我们的程序很小的时候(比如只有main函数),内存空间都用来运行main函数,就可能不会出现访问非法地址而产生段错误的情况。但是,当程序很大时,即不只有main函数访问内存空间,此时出现段错误的可能性就很大
指针操作的良好习惯
1.动态内存申请之后,应该立即检查指针值是否为NULL,防止使用NULL指针。
int* p = (int*)malloc(6);
if(p!=NULL){//…}
free§;
2.释放掉指针的空间之后,立即将指针赋值为NULL,这样可以防止产生野指针,也可以避免多次释放同一段内存空间,因free函数遇到NULL时不执行任何操作。接上述代码,p = NULL;
3.任何与内存操作相关的函数都必须带长度信息,即加一个size形参来辅助防止越界
4.malloc操作和free操作必须匹配,即要成对存在
5.尽量不要跨函数来free内存,否则可能会因在当前函数之前free过该段内存且没有将指针置为NULL而造成多次free非法内存地址而使程序崩溃
C第四十四课
函数参数的秘密
1.C语言中操作数的求值顺序不一定从左到右,其依赖于编译器的实现。
如:int k = 1; printf(“%d,%d\n”,k++,k++);//可能输出2,1也有可能是1,2
程序中的顺序点
1.程序中存在一定的顺序点
2.顺序点指的是执行过程中修改变量值的最晚时刻
3.在程序到达顺序点的时候,之前所做的一切操作必须完成
C语言中的顺序点:
1.每个完整表达式结束时,即分号处
2.&&,||,?:,以及逗号表达式的每个参数计算之后
3.函数调用时所有实参求值完成后(进入函数体之前)
示例1:
int k = 2;
k = k++ + k++;
printf(“k = %d\n”,k);
上述代码中第二行中有=,++,+这三个操作符会改变k的值,最后的分号是顺序点。该版本的编译器内部操作顺序是:先将k的值取出来,即2,把两个2相加赋值给k,k变成了4,加号两边的两个++操作在这个过程中被悬挂着,还没有被执行,当遇到分号这个顺序点的时候两个++操作生效,k自增两次变成6。不同编译器的执行结果可能不同。
示例2:
void func(int i,int j)
{
printf(“%d,%d\n”,i,j);
}
int main()
{
int k = 1;
func(k++,k++);
printf(“%d\n”,k);
return 0;
}
打印结果为
1,1
3
C第四十五课
调用约定
1.当函数调用发生时
(1)参数会传递给被调用的函数
(2)返回值会被返回给函数调用者
2.调用约定描述参数如何传递到栈中以及栈的维护方式
(1)参数传递顺序(形参都从左到右还是都从右到左入栈)
(2)调用栈清理(函数返回后清除其活动记录)
3.常用的调用约定
(1)从右到左依次入栈:_stdcall,_cdecl,_thiscall
(2)从左到右依次入栈:_pascal,_fastcall
PS:C语言编译器用_cdecl约定,即从右到左。调用约定不是C语言本身的一部分,是编译器的。
调用约定一般用在什么地方?
当我们在项目中要用到第三方库文件时,就要检查第三方库文件的调用约定跟自己的是否一致,否则会出错。
可变参数
#include <stdio.h>
#include <stdarg.h>
float average(int n,...)
{
va_list args;
int i = 0;
float sum = 0;
va_start(args,n);
for(i = 0;i<n;i++)
{
sum += va_arg(args,int);
}
va_end(args);
return sum/n;
}
int main()
{
printf(“%f\n”,average(5,1,2,3,4,5));
printf(“%f\n”,average(4,1,2,3,4));
return 0;
}
上述代码中,va_list定义得到一个集合名为args;va_start中的第二个参数n必须是…之前的那个,目的是为了得到那些自定义参数起始地址;va_arg命令从args集合中每次取出一个int类型的数据;va_end用来关闭args集合
可变参数的限制
1.可变参数必须从头到尾按照顺序逐个访问
2.参数列表中至少存在一个确定类型的命名参数
3.可变参数函数无法确定实际存在的参数的数量
4.可变参数函数无法确定参数的实际类型
注意:va_arg中如果指定了错误的类型,那么结果是不可预测的
C第四十六课
函数与宏
1.宏是由预处理器直接替换展开的,编译器不知道宏的存在
2.函数是由编译器直接编译的实体,调用行为由编译器决定
3.多次使用宏会导致最终可执行程序的体积增大
4.函数是跳转执行的,内存中有一份函数体存在
5.宏的效率比函数高,因为是直接展开,无调用开销
6.函数调用时会创建活动记录,效率比如宏
7.宏的定义中不能出现递归定义
宏的妙用
#define MALLOC(type,x) (type*)malloc(sizeof(type)*x)
#define FREE(p) (free(p),p = NULL)
#define LOG_STRING(s) printf(“%s = %s\n”,#s,s)
#define FREACH(i,n) while(1){ int i = 0,l = n;for(i=0,i<l;i++)
#define BEGIN {
#define END }break;}
为什么要用while?
因为要保证有一个代码块出来,这样变量i和l只在该代码块中,多次使用这个宏的时候不会出现重复定义,且break保证了while只循环一次*/