一,If,while,switch,do,for语句分析
1》分支语句分析 -- if。
if语句用于根据条件选择执行语句。
else不能独立存在且总是与它最近的if相匹配。
else语句后可以接连其他if语句。
if语句中零值比较的注意点。
bool型变量应该直接出现于条件中,不要进行比较。
普通变量和0值比较时,0值应该出现在比较符号左边。
float型变量不能直接进行0值比较,需要定义精度。
2》分支语句分析 -- switch
switch语句对应单个条件多个分值的情形。
每个case语句分支必须要有break,否则会导致分支重叠。
default语句有必要加上,以处理特殊情况。
case语句中的值只能是整型或字符型。
case语句排列顺序分析。
按字母或数字顺序排列各条语句。
正常情况放在前面,异常情况放在后面。
default语句只用于处理真正的默认情况。
分支语句分析小结
if语句实用于需要“按片”进行判断的情形中。
switch语句实用于需要对各个离散值进行分别判断的情形中。
if语句可以完全从功能上代替switch语句,但switch语句无法代替if语句。
switch语句对于多分支判断的情形更加简洁。
3》循环语句分析
1》循环语句的基本工作方式
通过条件表达式判定是否执行循环体。
条件表达式遵循if语句表达式的原则。
2》do,while,for的区别
do语句先执行后判断,循环体至少执行一次。
while语句先判断后执行,循环体可能不执行。
for语句先判断后执行,相比while更简洁。
break和continue的区别
break表示终止循环的执行。
continue表示终止本次循环体,进入下次循环执行。
Switch不能用continue关键字。
二.goto,void,extern,sizeof分析
1》Goto语句分析
高手潜规则:禁用goto。
项目经验:程序质量与goto的出现次数成反比。
最后的判决:将goto打入冷宫。
2》void的意义
void修饰函数返回值和参数。
如果函数没有返回值,那么应该将其声明为void型。
如果函数没有参数,应该声明其参数为void。
void修饰函数返回值和参数仅为了表示无。
C语言没有定义void究竟是多大内存的别名。
void指针的意义
C语言规定只有相同类型的指针才可以相互赋值。
void*指针作为左值用于“接收”任意类型的指针。
void*指针作为右值赋值给其它指针时需要强制类型转换。
3》extern中隐藏的意义
extern用于声明外部定义的变量和函数。
extern用于“告诉”编译器用C方式编译。
C++编译器和一些变种C编译器默认会按“自己”的方式编译
函数和变量,通过extern关键可以命令编译器“以标准C方
式进行编译”。
4》sizeof关键字分析
Sizeof是编译器的内置指示符,不是函数。
sizeof用于“计算”相应实体所占的内存大小。
sizeof的值在编译期就已经确定。
5》const修饰变量
在C语言中const修饰的变量是只读的,其本质还是变量。
const修饰的变量会在内存占用空间。
本质上const只对编译器有用,在运行时无用。
const不是真的常量,可以通过指针改变其值。
在C语言中const修饰的数组是只读的。
const修饰的数组空间不可被改变。
const int* p; //p可变,p指向的内容不可变
int const* p; //p可变,p指向的内容不可变
int* const p; //p不可变,p指向的内容可变
const int* const p; //p和p指向的内容都不可变
口诀:左数右指
当const出现在*号左边时指针指向的数据为常量。
当const出现在*后右边时指针本身为常量。
const修饰函数参数和返回值
const修饰函数参数表示在函数体内不希望改变参数的值。
const修饰函数返回值表示返回值不可改变,多用于返回
指针的情形。
6》深藏不漏的volatile
volatile可理解为“编译器警告指示字”。
volatile用于告诉编译器必须每次去内存中取变量值。
volatile主要修饰可能被多个线程访问的变量。
volatile也可以修饰可能被未知因数更改的变量。
三.Struct分析
由结构体产生柔性数组,柔性数组即数组大小待定的数组。
C语言中结构体的最后一个元素可以是大小未知的数组。
C语言中可以由结构体产生柔性数组。
struct占用的内存大小
第一个成员起始于0偏移处。
每个成员按其类型大小和指定对齐参数n中较小的一个进行对齐。
偏移地址和成员占用大小均需对齐。
结构体成员的对齐参数为其所有成员使用的对齐参数的最大值。
结构体总长度必须为所有对齐参数的整数倍。
1》union和struct的区别
struct中的每个域在内存中都独立分配空间。
union只分配最大域的空间,所有域共享这个空间。
union的使用受系统大小端的影响。
四.枚举类型的使用方法
enum是一种自定义类型。
enum默认常量在前一个值的基础上依次加1。
enum类型的变量只能取定义时的离散值。
五.枚举类型和#define的区别
#define宏常量只是简单的进行值替换,枚举常量是真正意义上的常量。
#define宏常量无法被调试,枚举常量可以。
#define宏常量无类型信息,枚举常量是一种特定类型的常量。
六.typedef的意义
typedef用于给一个已经存在的数据类型重命名。
typedef并没有产生新的类型。
typedef重定义的类型不能进行unsigned和signed扩展。
typedef和#define的区别
typedef是给已有类型取别名。
#define为简单的字符串替换,无别名的概念。
七.注释符号
编译器会在编译过程删除注释,但不是简单的删除而是用空格代替。
编译器认为双引号括起来内容都是字符串,双斜杠也不例外。
“/*……*/”型注释不能被嵌套。
注释应该准确易懂,防止二义性,错误的注释有害而无利 。
注释是对代码的提示,避免臃肿和喧宾夺主。
一目了然的代码避免加注释。
不要用缩写来注释代码,这样可能会产生误解。
注释用于阐述原因而不是用于描述程序的运行过程。
八.接续符和转义符
编译器会将反斜杠剔除,跟在反斜杠后面的字符自动解到前一行。
在接续单词时,反斜杠之后不能有空格,反斜杠的下一行之
前也不能有空格。
接续符适合在定义宏代码块时使用(C语言中定义宏函数的定义只能在 一行内完成)。
C语言中的反斜杠()同时具有接续符和转义符的作用。
当反斜杠作为接续符使用时可直接出现在程序中。
当反斜杠作为转义符使用时需出现在字符或字符串中。
九.单引号和双引号
中的单引号用来表示字符常量。
C语言中的双引号用来表示字符串常量。
‘a’表示字符常量在内存中占1个字节。
’a’+1表示’a’的ASCII码加1,结果为‘b’。
“a”表示字符串常量在内存中占2个字节。
“a”+1表示指针运算,结果指向“a”结束符’ ’。
本质上单引号括起来的一个字符代表整数。
双引号括起来的字符代表一个指针。
C编译器接受字符和字符串的比较,可意义是错误的。
C编译器允许字符串对字符变量赋值,其意义是可笑的。
十.接续符和转义符
编译器会将反斜杠剔除,跟在反斜杠后面的字符自动解到前
一行。
在接续单词时,反斜杠之后不能有空格,反斜杠的下一行之
前也不能有空格。
接续符适合在定义宏代码块时使用。
C语言中的反斜杠()同时具有接续符和转义符的作用。
当反斜杠作为接续符使用时可直接出现在程序中。
当反斜杠作为转义符使用时需出现在字符或字符串中。
十一.位运算符分析
左移运算符<<将运算数的二进制位左移
规则:高位丢弃,低位补0
右移运算符>>把运算数的二进制位右移。
规则:高位补符号位,低位丢弃
小技巧:
左移n位相当于乘以2的n次方,但效率比数学运算符高。
右移n位相当于除以2的n次方,但效率比数学运算符高。
十二.类型转换分析
算术运算式中,低类型转换为高类型。
赋值表达式中,表达式的值转换为左边变量的类型。
函数调用时,实参转换为形参的类型。
函数返回值,return表达式转换为返回值类型。
十三.编译器做了什么
预编译
处理所有的注释,以空格代替。
将所有的#define删除,并且展开所有的宏定义。
处理条件编译指令#if, #ifdef, #elif, #else, #endif。
处理#include,展开被包含的文件。
保留编译器需要使用的#pragma指令。
编译
对预处理文件进行一系列词法分析,语法分析和语义分析。
词法分析主要分析关键字,标示符,立即数等是否合法。
语法分析主要分析表达式是否遵循语法规则。
语义分析在语法分析的基础上进一步分析表达式是否合法。
分析结束后进行代码优化生成相应的汇编代码文件。
汇编
汇编器将汇编代码转变为机器可以执行的指令。
每个汇编语句几乎都对应一条机器指令。
十四.定义宏常量
#define定义宏常量可以出现在代码的任何地方。
#define从本行开始,之后的代码都可以使用这个宏常量。
#define表达式给有函数调用的假象,却不是函数。
#define表达式可以比函数更强大。
#define表达式比函数更容易出错。
宏表达式在预编译期被处理,编译器不知道宏表达式的存在。
宏表达式用“实参”完全替代形参,不进行任何运算。
宏表达式没有任何的“调用”开销。
宏表达式中不能出现递归定义。
====定义完整的日志文件宏
#define LOG(s) do{ time_t t; struct tm* tim; time(&t); tim = localtime(&t); printf("%s:%d,%s,%s ",__FILE__,__LINE__,asctime(tim),s); }while(0)
十五.条件编译的行为
1.条件编译的行为类似于C语言中的if…else。
2. 条件编译是预编译指示命令,用于控制是否编译某段代码。
3.#include的本质是将已经存在的文件内容嵌入到当前文件中。
4.#include的间接包含同样会产生嵌入文件内容的动作。
5.条件编译使得我们可以按不同的条件编译不同的代码段,因而可
以产生不同 的目标代码。
6.#if…#else…#endif被预编译器处理;而if…else语句被编译器处
理,必然被编译进目标代码。
7.通过编译器命令行能够定义预处理器使用的宏。
8.条件编译可以避免重复包含头同一个头文件。
9. 条件编译是在工程开发中可以区别不同产品线的代码。
10. 条件编译可以定义产品的发布版和调试版。
十六.#error和#line的用法
#error用于生成一个编译错误消息,并停止编译。
用法:#error message
注:message不需要用双引号包围
#error编译指示字用于自定义程序员特有的编译错误消息。
类似的,#warning用于生成编译警告,但不会停止编译。
#line用于强制指定新的行号和编译文件名,并对源程序的代码重新编号。
? 用法:#line number filename
注:filename可省略
#line编译指示字的本质是重定义__LINE__和__FILE__。
十七.#pragma简介
1. #pragma是编译器指示字,用于指示编译器完成一些特定的动作。
2.#pragma所定义的很多指示字是编译器和操作系统特有的。
3.#pragma在不同的编译器间是不可移植的。
4.预处理器将忽略它不认识的#pragma指令。
5.两个不同的编译器可能以两种不同的方式解释同一条#pragma指令。
6. #pragma pack能够改变编译器的默认对齐方式。
一般用法:#pragma parameter
注:不同的parameter参数语法和意义各不相同
#pragma message
message参数在大多数的编译器中都有相似的实现。
message参数在编译时输出消息到编译输出窗口中。
message可用于代码的版本控制。
什么是内存对齐?#pragma pack
不同类型的数据在内存中按照一定的规则排列;而不是顺序的一个
接一个的排放,这就是对齐。
为什么需要内存对齐?
1.CPU对内存的读取不是连续的,而是分成块读取的,块的大小只能
是1、2、4、8、16字节。
2.当读取操作的数据未对齐,则需要两次总线周期来访问内存,因
3此性能会大打折扣。
4. 某些硬件平台只能从规定的地址处取某些特定类型的数据,否则
抛出硬件异常。
十八.指针本质
1.指针在本质上也是一个变量,需要占用一定的内存空间。指针用于保
存内存地址的值。
*号的意义
1.在指针声明时,*号表示所声明的变量为指针。
2.在指针使用时,*号表示取指针所指向的内存空间中的值。
3.号类似一把钥匙,通过这把钥匙可以打开内存,读取内存中的值。
传值调用与传址调用
1.指针是变量,因此可以声明指针参数。
2. 当一个函数体内部需要改变实参的值,则需要使用指针参数。
3.函数调用时实参值将复制到形参。
4.指针适用于复杂数据类型作为参数的函数中。
5. 指针是C语言中一种特别的变量。
6.指针所保存的值是内存的地址。
7.可以通过指针修改内存中的任意地址内容。
十九.数组的概念
1.数组是相同类型的变量的有序集合int a[5]。
2.数组在一片连续的内存空间中存储元素。
3. 数组元素的个数可以显示或隐式指定。
4.数组名代表数组首元素的地址。
5.数组的地址需要用取地址符&才能得到。
6.数组首元素的地址值与数组的地址值相同。
7.数组首元素的地址与数组的地址是两个不同的概念。。
9数组名可以看做一个常量指针。
10. 数组名“指向”的是内存中数组首元素的起始位置。
11. 在表达式中数组名只能作为右值使用。
12. 只有在下列场合中数组名不能看做常量指针。
数组名作为sizeof操作符的参数。
数组名作为&运算符的参数。
13.数组是一片连续的内存空间。
14. 数组的地址和数组首元素的地址意义不同。
15. 数组名在大多数情况下被当成常量指针处理。
16. 数组名其实并不是指针,在外部声明时不能混淆。
数组的本质
数组是一段连续的内存空间。
数组的空间大小为sizeof(array_type) * array_size。
数组名可看做指向数组第一个元素的常量指针。
指针的运算
指针是一种特殊的变量,与整数的运算规则为:
p + n; (unsigned int)p + n*sizeof(*p);
结论:
当指针p指向一个同类型的数组的元素时:p+1将指向。
当前元素的下一个元素;p-1将指向当前元素的上一个元素。
指针的运算
指针之间只支持减法运算,且必须参与运算的指针类型必须相同
p1 – p2; == ( (unsigned int)p1 - (unsigned int)p2) / sizeof(type);
注意:
只有当两个指针指向同一个数组中的元素时,指针相减才有意义,
其意义为指针所指元素的下标差。
当两个指针指向的元素不在同一个数组中时,结果未定义。
指针的比较
指针也可以进行关系运算:
< <= > >=
指针关系运算的前提是同时指向同一个数组中的元素。
任意两个指针之间的比较运算(==, !=)无限制。
下标 VS 指针
理论上而言,当指针以固定增量在数组中移动时,其效率高于下标产生的代码
当指针增量为1且硬件具有硬件增量模型时,表现更佳
注意:
现代编译器的生成代码优化率已大大提高,在固定增量时,下标形式的
效率已经和指针形式相当;但从可读性和代码维护的角度来看,下标形式更优。
a和&a的区别
a为数组是数组首元素的地址。
&a为整个数组的地址。
a和&a的意义不同其区别在于指针运算:
a + 1 (unsigned int)a + sizeof(*a)
&a + 1 (unsigned int)(&a) + sizeof(*&a)
数组参数
C语言中,数组作为函数参数时,编译器将其编译成对应的指针
void f(int a[]); void f(int* a);
void f(int a[5]); void f(int* a);
结论:
一般情况下,当定义的函数中有数组参数时,需要定义另一个参数来标示
数组的大小。
指针和数组的对比
数组声明时编译器自动分配一片连续内存空间。
指针声明时只分配了用于容纳指针的4字节空间。
在作为函数参数时,数组参数和指针参数等价。
数组名在多数情况可以看做常量指针,其值不能改变。
指针的本质是变量,保存的值被看做内存中的地址。
二十.C语言中的字符串
从概念上讲,C语言中没有字符串数据类型。
在C语言中使用字符数组来模拟字符串。
C语言中的字符串是以’ ’结束的字符数组。
C语言中的字符串可以分配于栈空间,堆空间或者只读存储区。
字符串长度
字符串的长度就是字符串所包含字符的个数。
C语言中的字符串长度指的是第一个’ ’字符前出现的字符个数。
C语言中通过’ ’结束符来确定字符串的长度。
strlen的返回值是用无符号数定义的,因此相减不可能产生负数。
一般情况下,千万千万不要自行编写C标准库已经提供的函数。
标准库有时会使用汇编语言实现,目的就是为了充分利用机器所提供的
特殊指令以追求最大的速度。
复用已经存在的函数库会更高效。
长度不受限的字符串函数
不受限制的字符串函数是通过寻找字符串的结束符’ ’来判断长度
字符串复制: char* strcpy(char* dst, const char* src);
字符串连接: char* strcat(char* dst, const char* src);
字符串比较: int strcmp(const char* s1, const char* s2);
不受限制的字符串函数都是以‘ ’作为结尾标记来进行的,因此输入参
数中必须包含’ ’。
strcpy和strcat必须保证目标字符数组的剩余空间足以保存整个源字符串。
strcmp以0值表示两个字符串相等。
第一个字符串大于第二个字符串的时候返回值大于0。
第一个字符串小于第二个字符串的时候返回值小于0。
strcmp不会修改参数值,但依然以’ ’作为结束符。
长度受限的字符串函数
长度受限的字符串函数接收一个显示的长度参数用于限定操作的字符数
字符串复制: char* strncpy(char* dst, const char* src, size_t len);
字符串连接: char* strncat(char* dst, const char* src , size_t len);
字符串比较: int strncmp(const char* s1, const char*s2 , size_t len)
strncpy只复制len个字符到目标字符串。
当源字符串的长度小于len时,剩余的空间以’ ’填充。
当源字符串的长度大于len时,只有len个字符会被复制,且它将不会以
’ ’ 结束。
strncat最多从源字符串中复制len个字符到目标串中。
strncat总是在结果字符串后面添加’ ’。
strncat不会用’ ’填充目标串中的剩余空间。
strncmp只比较len个字符是否相等。
二十一.指针数组和数组指针分析
数组类型
C语言中的数组有自己特定的类型。
数组的类型由元素类型和数组大小共同决定。
例:int array[5]的类型为int[5]。
定义数组类型
C语言中通过typedef为数组类型重命名typedef type(name)[size];
数组类型: typedef int(AINT5)[5];
typedef float(AFLOAT10)[10];
数组定义: AINT5 iArray;
AFLOAT10 fArray
1.数组指针
数组指针用于指向一个数组。
数组名是数组首元素的起始地址,但并不是数组的起始地址。
通过将取地址符&作用于数组名可以得到数组的起始地址。
可通过数组类型定义数组指针: ArrayType* pointer;
也可以直接定义:type (*pointer)[n];。
pointer为数组指针变量名。type为指向的数组的类型。n为指向的数组的大小。
2.指针数组
指针数组是一个普通的数组。
指针数组中每个元素为一个指针。指针数组的定义:type* pArray[n]。
type*为数组中每个元素的类型。pArray为数组名。 n为数组大小。
main函数的参数
int main()
int main(int argc)
int main(int argc, char *argv[])
int main(int argc, char *argv[], char *env[])
argc –命令行参数个数。
argv –命令行参数数组。
env –环境变量数组。
数组指针本质上是一个指针。
数组指针指向的值是数组的地址。
指针数组本质上是一个数组。
指针数组中每个元素的类型是指针。
二十二.多维数组和多维指针
1.指向指针的指针
指针变量在内存中会占用一定的空间。
可以定义指针来保存指针变量的地址值。
2.指向指针的指针
指针在本质上也是变量。对于指针也同样存在传值调用与传址调用。
3.二维数组与二级指针
二维数组在内存中以一维的方式排布。
二维数组中的第一维是一维数组。
二维数组中的第二维才是具体的值。
二维数组的数组名可看做常量指针。
4.数组名
一维数组名代表数组首元素的地址 ,int a[5] a的类型为int*。
二维数组名同样代表数组首元素的地址,int m[2][5] m的类型为int(*)[5]。
结论:
1. 二维数组名可以看做是指向数组的常量指针。
2. 二维数组可以看做是一维数组。
3. 二维数组中的每个元素都是同类型的一维数组。
小结
C语言中只有一维数组,而且数组大小必须在编译期就作为常数确定。
C语言中的数组元素可是任何类型的数据,即,数组的元素可以是另一个数组。
C语言中只有数组的大小和数组首元素的地址是编译器直接确定的。
#include <stdio.h> #include <assert.h> #include <string.h> #include <malloc.h> typedef int(arrer5)[5]; arrer5 shuzu; int array_int[5]= {1,2,3,4,5}; unsigned char* array[5] = { "aaa", "bbb", "ccc", "ddd", "eee" }; size_t str_len(const char* s) { return (assert(s),(*s ? (str_len(s+1)+1) : 0)) ; } int reset(char** p,int old_size,int new_size) { char* new = NULL; char* pt = NULL; char* pp = *p; int i = 0,ret = 1; int len = 0; if(p != NULL && new_size > 0) { pt = (char*)malloc(new_size); new = pt; len = ((old_size < new_size) ? old_size : new_size); for(i=0;i<len;i++) { *new++ = *pp++; } free(*p); *p = pt; } else ret = 0; return ret; } void str_copy1(char* s1,char* s2) { int size1,size2; int i,j; assert(s1 && s2); size1 = strlen(s1); size2 = strlen(s2); for(i=size1,j = 0;s2[j] != ' ';i++,j++) { s1[i] = s2[j]; } s1[size1+size2] = ' '; printf("the str1 len is:%d ",size1); printf("the str2 len is:%d ",size2); printf("the add s1 len is:%d ",str_len(s1)); } char* str_copy2(char* s1,char* s2) { assert(s1 && s2); while((*s1++ = *s2++) != ' '); return s1; } void text_fun() { int i; char str1[100]; char str2[] = "AAAAAAAAAAAAAAA "; str_copy2(str1,str2); printf("%s ",str1); for(i=0;i<5;i++) *(array+0) = (unsigned int)(array_int+0); printf("%0x",array[0]); printf(" %0x ",&array_int[0]); printf("%s",array[1]); printf(" %0x ",&array_int[1]); printf("%s",array[2]); printf(" %0x ",&array_int[2]); printf("%s",array[3]); printf(" %0x ",&array_int[3]); printf("%s",array[4]); printf(" %0x ",&array_int[4]); } int main() { int i; char* pint = (char*)malloc(5); printf("old size address:%0x ",pint); reset(&pint, 5, 7); printf("new size address:%0x ",pint); }
二十三.数组参数和指针参数分析
退化的意义
C语言中只会以值拷贝的方式传递参数。
当向函数传递数组时, 将数组名看做常量指针传数组首元素地址。
二维数组参数
二维数组可以看做是一维数组,二维数组中的每个元素是一维数组。
二维数组参数中第一维的参数可以省略:
void f(int a[5]); == void f(int a[]); == void f(int* a);
void g(int a[3][3]); == void g(int a[][3]); == void g(int (*a)[3]);
等价关系
数组参数 |
等效的指针参数 |
一维数组:float a[5] |
指针:float* a |
指针数组:int* a[5] |
指针的指针:int** a |
二维数组:char a[3][4] |
数组的指针:char (*a)[4] |
注意事项
C语言中无法向一个函数传递任意的多维数组
为了提供正确的指针运算,必须提供除第一维之外的所有维长度限制
一维数组参数 – 必须提供一个标示数组结束位置的长度信息
二维数组参数 – 不能直接传递给函数
三维或更多维数组参数 – 无法使用
二十四.函数与指针分析
1.函数类型
C语言中的函数有自己特定的类型,函数的类型由返回值,参数类型和参数个数共同决定。
例:int add(int i, int j)的类型为int(int, int)
C语言中通过typedef为函数类型重命名:typedef type name(parameter list)
例: typedef int f(int, int);
typedef void p(int);
2.函数指针
函数指针用于指向一个函数。函数名是执行函数体的入口地址。
可通过函数类型定义函数指针: FuncType* pointer;。
也可以直接定义:type (*pointer)(parameter list);
pointer为函数指针变量名。
type为指向函数的返回值类型。
parameter list为指向函数的参数类型列表。
3.回调函数
回调函数是利用函数指针实现的一种调用机制。
回调机制原理。
调用者不知道具体事件发生的时候需要调用的具体函数。
被调函数不知道何时被调用,只知道被调用后需要完成的任务。
当具体事件发生时,调用者通过函数指针调用具体函数。
回调机制的将调用者和被调函数分开,两者互不依赖。
4.指针阅读技巧解析-- 右左法则
1. 从最里层的圆括号中未定义的标示符看起。
2. 首先往右看,再往左看。
3. 当遇到圆括号或者方括号时可以确定部分类型,并调转方向。
4. 重复2,3步骤,直到阅读结束。
二十五.动态内存分配
1.C语言中的一切操作都是基于内存的变量和数组都是内存的别名,如何分
配 这些内存由编译器在编译期间决定。
2.定义数组的时候必须指定数组长度而数组长度是在编译期就必须决定的。
需求:程序运行的过程中,可能需要使用一些额外的内存空间。
malloc和free
1.malloc所分配的是一块连续的内存,以字节为单位,并且不带任何的类型信息 。2.free用于将动态内存归还系统。
void* malloc(size_t size);
void free(void* pointer);
注意:
1.malloc实际分配的内存可能会比请求的稍微多一点,但是不能依赖于编译器
的这个行为。
2.当请求的动态内存无法满足时malloc返回NULL。
2.当free的参数为NULL时,函数直接返回。
calloc和realloc
void* calloc(size_t num, size_t size);
void* realloc(void* pointer, size_t new_size);
1. calloc的参数代表所返回内存的类型信息。
2. calloc会将返回的内存初始化为0。
3.realloc用于修改一个原先已经分配的内存块大小。
4.在使用realloc之后应该使用其返回值。
5.当pointer的第一个参数为NULL时,等价于malloc。
小结
1.动态内存分配是C语言中的强大功能。
2. 程序能够在需要的时候有机会使用更多的内存。
3.malloc单纯的从系统中申请固定字节大小的内存。
4.calloc能以类型大小为单位申请内存并初始化为0。
5.realloc用于重置内存大小。
二十六.程序中的三国天下
程序中的栈
1. 栈是现代计算机程序里最为重要的概念之一。
2. 栈在程序中用于维护函数调用上下文,没有栈就没有函数,没有局部变量。
3. 栈保存了一个函数调用所需的维护信息
函数参数,函数返回地址
局部变量
函数调用上下文
程序中的堆
1. 为什么有了栈还需要堆。
2. 栈上的数据在函数返回后就会被释放掉,无法传递到函数外部,局部数组。
3. 堆是程序中一块巨大的内存空间,可由程序自由使用。
4. 堆中被程序申请使用的内存在程序主动释放前将一直有效。
5.系统对堆空间的管理方式:空闲链表法,位图法,对象池法等等。
程序中的静态存储区
1. 程序静态存储区随着程序的运行而分配空间,直到程序运行结束。
2. 在程序的编译期静态存储区的大小就已经确定。
3. 程序的静态存储区主要用于保存程序中的全局变量和静态变量。
4.与栈和堆不同,静态存储区的信息最终会保存到可执行程序中。
小结
1.栈,堆和静态存储区是C语言程序常涉及的三个基本内存区。
2.栈区主要用于函数调用的使用。
3. 堆区主要是用于内存的动态申请和归还。
4. 静态存储区用于保存全局变量和静态变量。
二十七.程序的内存布局
各个段的作用
1.堆栈段在程序运行后才正式存在,是程序运行的基础。
2. .bss段存放的是未初始化的全局变量和静态变量。
3. .text段存放的是程序中的可执行代码。
4..data段保存的是那些已经初始化了的全局变量和静态变量。
5. .rodata段存放程序中的常量值,如字符串常量。
程序术语对应关系
1. 静态存储区通常指程序中的.bss和.data段。
2. 只读区通常指程序中的.rodata段。
3. 局部变量所占空间为栈上空间。
4..动态空间为堆中的空间。
5.程序可执行代码存放于.text段。
二十八.野指针和内存操作分析
初识野指针
1. 野指针通常是因为指针变量中保存的值不是一个合法的内存地址而造成的。
2. 野指针不是NULL指针,是指向不可用内存的指针。
3. NULL指针不容易用错,因为if语句很好判断一个指针是不是NULL。
野指针的由来
1.局部指针变量没有被初始化。
2. 使用已经释放过后的指针。
3. 指针所指向的变量在指针之前被销毁。
非法内存操作分析
1. 结构体成员指针未初始化。
2. 没有为结构体指针分配足够的内存。
3..内存分配成功,但并未初始化。
4..数组越界。
C语言中的交通规则
1. 用malloc申请了内存之后,应该立即检查指针值是否为NULL,防止使用值为NULL的指针。
2. 牢记数组的长度,防止数组越界操作,考虑使用柔性数组。
3. 动态申请操作必须和释放操作匹配,防止内存泄露和多次释放。
4. ? free指针之后必须立即赋值为NULL。
二十九.认清函数的真面目
面向过程的程序设计
1.面向过程是一种以过程为中心的编程思想。
2.首先将复杂的问题分解为一个个容易解决的问题。
3.分解过后的问题可以按照步骤一步步完成。
4.函数是面向过程在C语言中的体现。
5.解决问题的每个步骤可以用函数来实现。
声明和定义
1.程序中的声明可理解为预先告诉编译器实体的存在,如:变量,函数,等等。
2.程序中的定义明确指示编译器实体的意义。
函数参数
1.函数参数在本质上与局部变量相同,都是在栈上分配空间。
2. 函数参数的初始值是函数调用时的实参值。
程序中的顺序点
1. 程序中存在一定的顺序点。
2.顺序点指的是执行过程中修改变量值的最晚时刻。
3. 在程序达到顺序点的时候,之前所做的一切操作必须反映到后续的访问中。
C语言中的顺序点
1. 每个完整表达式结束时。
2. &&, ||, ?:, 以及逗号表达式的每个运算对象计算之后。
3. 函数调用中对所有实际参数的求值完成之后(进入函数体之前)。
4.? C语言会默认没有类型的函数参数为int。
小结
1. C语言是一种面向过程的语言。
2. 函数可理解为解决问题的步骤。
3. 函数的实参并没有固定的计算次序。
4. 顺序点是C语言中变量改变的最晚时机。
5. 函数定义时参数和返回值的缺省类型为int。
三十.可变参数分析与宏分析
可变参数
1.C语言中可以定义参数可变的函数。
2.参数可变函数的实现依赖于stdarg.h头文件。
3.va_list变量与va_start, va_end和va_arg配合使用能够访问参数值。
可变参数的限制
1. 可变参数必须从头到尾按照顺序逐个访问。
2. 参数列表中至少要存在一个确定的命名参数。
3. 可变参数宏无法判断实际存在的参数的数量。
4. 可变参数宏无法判断参数的实际类型。
小结
1.可变参数是C语言提供的一种函数设计技巧。
2. 可变参数的函数提供了一种更方便的函数调用方式。
3. 可变参数必须顺序的访问。
4. 无法直接访问可变参数列表中间的参数值。
函数 VS 宏
1. 宏是由预处理直接替换展开的,编译器不知道宏的存在。
2. 函数是由编译器直接编译的实体,调用行为由编译器决定。
3. 多次使用宏会导致程序代码量增加。
4. 函数是跳转执行的,因此代码量不会增加。
5. 宏的效率比函数要高,因为是直接展开,无调用开销。
6. 函数调用时会创建活动记录,效率不如宏。
7.宏的效率比函数稍高,但是其副作用巨大,容易出错。
函数优点和缺点
1.函数存在实参到形参的传递,因此无任何副作用,但是函数需要建立活动对象,效率受影响。
宏无可替代的优势
1.宏参数可以是任何C语言实体。
宏编写的_MIN_参数类型可以是int, float等等。
宏的参数可以是类型名。
小结
1. 宏和函数并不是竞争对手。
2. 宏能够接受任何类型的参数,效率高,易出错。
3. 函数的参数必须是固定类型,效率稍低,不易出错。
4. 宏可以实现函数不能实现的功能。
三十一.函数调用行为
活动记录
1.活动记录是函数调用时用于记录一系列相关信息的记录。
2. 临时变量域:用来存放临时变量的值,如k++的中间结果。
3. 局部变量域:用来存放函数本次执行中的局部变量。
4. 机器状态域:用来保存调用函数之前有关机器状态的信息,包括
各种寄存器的当前值和返回地址等;。
5. 实参数域:用于存放函数的实参信息。
6. 返回值域:为调用者函数存放返回值。
调用约定
1.当一个函数被调用时,参数会传递给被调用的函数,而返回值会被返回给调
用函数。函数的调用约定就是描述参数。是怎么传递到栈空间的,以及栈空
间由谁维护。
参数传递顺序
从右到左依次入栈:__stdcall,__cdecl,__thiscall。
从左到右依次入栈:__pascal,__fastcall。
调用堆栈清理
调用者清除栈。。
被调用函数返回后清除栈。
小结
1..函数调用是C语言的核心机制。
2. 活动记录中保存了函数调用以及返回所需要的一切信息。
3. 调用约定是调用者和被调用者之间的调用协议,常用于不同开发者编写的
库函数之间。
三十二.函数递归与函数设计技巧
递归概述
1.递归是数学领域中概念在程序设计中的应用。
2.递归是一种强有力的程序设计方法。
3.递归的本质为函数内部在适当的时候调用自身。
递归函数
1.C递归函数有两个主要的组成部分。
2.递归点 – 以不同参数调用自身。
3. 出口– 不在递归调用。
小结
1. C语言中的递归函数必然会使用判断语句。
2.递归函数在需要编写的时候定义函数的出口,否则栈会溢出。
3. 递归函数是一种分而治之的思想。
函数设计技巧
1. 不要在函数中使用全局变量,尽量让函数从意义上是一个独立的功能模块。
2. 参数名要能够体现参数的意义。
3.如果参数是指针,且仅作输入参数用,则应在类型前加const,以防止该
指针在函数体内被意外修改。
4. 不要省略返回值的类型,如果函数没有返回值,那么应声明为void类型。
5. 在函数体的“入口处”,对参数的有效性进行检查,对指针的检查尤为重要。
6. 语句不可返回指向“栈内存”的“指针”,因为该内存在函数体结束时被
自动销毁。
7. 函数体的规模要小,尽量控制在80行代码之内。
8. 相同的输入应当产生相同的输出,尽量避免函数带有“记忆”功能。
9. 避免函数有太多的参数,参数个数尽量控制在4个以内。
10. 有时候函数不需要返回值,但为了增加灵活性,如支持链式表达,可以
附加返回值。
11. 函数名与返回值类型在语义上不可冲突。
三十三.进军C++的世界
初识OOP
1. 类和对象是面向对象中的两个基本概念。
2. “类”指的是一类事物,是一个抽象的概念。
3.“对象”指的是属于某个类的一个实体,是一个具体存在的事物。
4. 类是一种“模板”,可以通过这种模板创建出不同的对象“实例”。
5.对象“实例”是类“模板”的一个具体实现。
6. 一个类可以有很多对象,而一个对象必然属于某个类。
抽象
1. 抽象的的意义是观察一群“事物”,并认识它们所具有的一些共同特性。
2. 抽象的本质是忽略不重要的区别,只记录能表现事物特征的关键数据项。
3. 类是抽象在程序设计领域的概念。
4. 类用于抽象的描述一类事物所特有的属性和行为 如:电脑类的每个对象
都有CPU,内存和硬盘,电脑类的每个对象都可以开机和运行程序。
5. 对象是一个具体的事物,拥有其所属类的所有属性,并且每个属性都
是一个特有的值。
6. 如:老虎的每个对象(也就是每只老虎),都有不同的体重,不同食量以
及不同的性情。
封装
1.类中描述的事物属性和行为往往是相关的。
2. 在C++中属性通过变量来表示,行为通过函数来模拟。
3. 封装指的是类中的变量只能通过类的函数来访问。
访问控制
1.C++的类中有三种访问权限。
2. public -- 类的外部可以自由的访问。
3. protected -- 类自身和子类中可以访问。
4. private -- 类自身中可以访问。
你也能做富二代
1.在C语言中struct有了自已的含义,虽然在C++中扩展成为了类,但一般情
况还是遵循C中的用法。
2.C++一般情况下用class来做类的关键字声明。
3. 继承是C++中代码复用的方式,通过继承,在子类中可以使用父类中的代码。
4. 子类可以完全继承父类中所有的变量和函数,在可以使用父类的地方就
可以用子类代替。
5. 子类从概念上而言是一种特殊的父类。
小结
1. 面向对象是一种新型的软件开发思想。
2..面向对象将生活中的事物完全映射到程序中。
3.抽象,封装和继承是面向对象程序设计的重要特性。
4. 继承能够很好的复用已有类的特性。
5.子类是一种特殊化的父类。