原文地址
前言
默认有基础,用于其他语言速通,或者归纳温习
字符编码
- 字符编码
- ASCII:单字节,基本字符编码
- GB2312:1-2个字节,兼容ASCII,单字节0x,双字节1x 1x
- GBK:1-2字节,兼容ASCII,相比于GB2312收录个更多字,比如峣,编码格式相同
- GB18030:1、2、4字节,兼容ASCII,单字节0x,双字节1x 0x,四字节1x 00x x x
- utf-8:1-4字节,兼容ASCII,单字节0x,双字节110x x,三字节1110x 10x 10x,四字节11110x 10x 10x 10x
- utf-16:2、4字节,不兼容ASCII,双字节,直接使用unicode字符集,四字节11010x x 11010x x
- utf-32:4字节,不兼容ASCII
- 字符集
- GB2312-80
- unicode
- utf
- utf-8:节省存储空间,节省流量,但是效率低,存储读取需要转换,不能随机存储读取
- utf-32:空间换效率,可以随机存储读取
- utf-16:折中方案,2字节的unicode不用转换
- tips
- 窄字符就是用一个字节表示一个字符,宽字符就是用>=2个字节表示一个字符,这个不适用于非英文字符,窄字符中文字符也是两个字节
- char不能直接存储中文,一个字节只支持ASCII,但是wchar_t宽字符可以,wchar_t在不同编译器下是不同类型别名,微软编译器下为unsigned short长度16,gcc下unsigned int 长度32,即微软编译器采用utf-16,gcc采用utf-32
- char字符串,即窄字符串,在windows中是用的是ansi本地编码(故中国大陆用的是gbk),而在gcc中是和源文件相同字符编码来保存
- 代码有源文件字符编码和运行字符编码,源文件字符编码用于保存到硬盘,传输等,故而需要省空间一般使用utf-8,大部分ide和编译器都是如此(vs采用ansi本地编码),而运行字符编码需要将数据加载到内存、cpu效率更加重要,一般采用utf16或者32
编译链接
- 预编译:.c -> .c,处理预编译指令,如#ifndef,#debug,inline等
- 编译:.c -> .o/.obj,将C语言代码转换成CPU能够识别的二进制指令
- 链接:.o/.obj -> .exe,将所有二进制形式的目标文件和系统组件组合成一个可执行文件
数据类型
Name | Size | Detail | |
---|---|---|---|
bool | 1 | ||
char | 8 | %c、%s | |
short | 16 | %hd、%ho、%hu、%hx | 十进制、八进制、unsigned、十六进制 |
int | 16/32 | %d、%o、%u、%x | |
long | 32/64 | %ld、%lo、%lu、%lx | 64位系统下,windows32Unix64 |
long long | 64 | %lld、%llo、%llu、%llx | |
float | 32 1-8-23 | %f、%e、%E、%g、%G | 十进制、带e、带E、短字符择优选择e/E |
double | 64 1-11-52 | %lf、%le、%lE、%lg、%lG | |
/ | / | %p、%P、%#llX/lX/X、%#llx/lx/x | 输出地址 |
ps: int一般为机器字长,这是在16/32位时代规定的,对于64位时代意义不大,所以现在的64位系统即使是在64位编译器下,int仍然位32位
- 类型转换
- 自动类型转换,小范围向大范围转,不需要特别处理可以自动转换,或者默认直接省略的计算,如整型自动向0取整,如ASCII的字符和数字的互转
- 强制类型转换,比如避免整型的自动向0取整,需要将一个数字强转成浮点数。强转不改变右值本身
关键字
printf、puts、sizeof、putchar、scanf、gets、getchar、getche、getch
输出输出
Name | Function | Detail |
---|---|---|
printf | output | |
puts | output | 只能输出字符串,并且输出结束后会自动换行 |
putchar | output | 只能输出单个字符 |
scanf | input | 读取%s的字符串,以空白符(tab/backsapce/enter)为结束 |
getchar | input | 读取单字符 |
gets | input | 读取%s的字符串,以回车为结束 |
缓冲
- 缓冲方式:
- 全缓冲:当缓冲区被填满以后才进行真正的输入输出操作,输入输出不涉及交互设备时,它们才可以是全缓冲的
- 行缓冲:输入或者输出的过程中遇到换行符时,才执行真正的输入输出操作
- 无缓冲:没有缓冲区
- 输入:scanf、gets,getchar为行缓冲,windows下的getche和getch无缓冲
- 输出:printf,putchar,puts在linux、mac下表现为行缓冲,windows表现为无缓冲
printf
- %[flag][width][.precision]type
- flag:
- -:左对齐,默认右对齐
- +:正值添加正号,默认不添加
- backspace:正值添加空格,默认不添加
- #:%o%x带进制号,%f%e%g小数不省略小数点
- width: 宽度,至少占用几个字符
- precision: 精度。整型前补零,后不管。小数,四舍五入截断或者补零。字符串,截断或者不管
- ex:
- %-10d、%.2lf、%.5s
- 刷新输出缓冲区,将缓冲区内容全部输出:fflush(stdout);
scanf
-
换行符也会进入缓冲区
-
记得变量前面加取地址符号&
-
%s,%d等等可以忽略重复空白符进行匹配,但是scanf中其他字符不行,比如scanf(“a=%d”, &a);可以匹配"a= 100",不能匹配" a=100"
-
匹配原则:
-
首先,输入缓冲区末位符号位\n,即输入缓冲区有输入且可读取,scanf将输入缓冲区加到内存
-
然后进行匹配,"%“相当于前面增加”[ \n\t]*"的正则
-
匹配成功后,如果匹配队列未空将匹配队列放回缓冲区,如果匹配队列已空等待用户输入缓冲区
-
如果匹配失败,将未匹配成功的部分放回缓冲区,但是同一条语句中已经被匹配的数值会被保留
//如果输入"100 @",则a仍然可以赋值成功 scanf("%d %d", &a, &b);
-
-
如果匹配中有脏数据会一直匹配失败,所以有时候需要清空匹配队列:
while((c = getchar()) != '\n' && c != EOF);
-
%[*][width]type
-
type:%d, %s, 也可以使用scanf匹配规则
-
width:读入数字位数/字符串宽度,多出位数/宽度会被放回缓冲区
-
*:丢弃匹配项,不予赋值
-
ex:
scanf("%*[^\n]"); //舍弃除换行符以外的所有字符,遇到换行符就停止舍弃 scanf("%[a-z-A-Z0-9]", str); //表示读取所有的英文字母和十进制数字 scanf("%30[^0-9\n]", str); //读取一行不能包含十进制数字的字符串,并且长度不能超过30
-
数组
数组初始化
int a[4] = {20, 345, 700, 22};
int b[10]={12, 19, 22 , 993, 344};
int a2[3][4];
int b2[5][3]={ {80,75,92}, {61,65,71}, {59,63,70}, {85,87,90}, {76,77,85} };
int b3[5][3]={80, 75, 92, 61, 65, 71, 59, 63, 70, 85, 87, 90, 76, 77, 85};
//初始化多于申请的内存会被赋值为0/'\0'/0.0
int nums[10] = {0};
char str[10] = {0};
float scores[10] = {0.0};
int a[3][3] = {{1}, {2}, {3}};
//自动获取内存空间大小
int c[] = {1, 2, 3, 4, 5};
char str1[] = "something justl like this";
int a[][3] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
//变量指定大小
int n;
scanf("%d", &n);
int arr[n];
字符数组
-
字符串可以直接被字符数组初始化是c语言的规定,并不是符合通常规则。一般情况是当"something just like this"作为右值时候,是将字符串放在常量区再返回常量区地址给左值,所以char* str是可以作为左值,但是由于来自于常量区所以不能更改字符串内容
-
但是在数组初始化中,左值为char a[],这里右值应该传入数据,而不是数据的指针。所以这里是c语言初始化字符数组的规定,所以在不是初始化时是不能这么赋值的
-
在C语言中,字符串总是以’\0’作为结尾,所以’\0’也被称为字符串结束标志,或者字符串结束符,由" "包围的字符串会自动在末尾添加’\0’,所以在使用字符串定义字符数组,如果选择手动给大小,需要比字符串本身长度多一个
char str[7] = "123456";
-
但是逐字符赋值是单纯的字符数组,不属于字符串转字符数组,不会带有’\0’
char str[] = {'a', 'b', 'c'}; //由于这种时字符数组而不是字符串,所以使用strlen不会得到有用的结果
-
字符串长度(strlen)是该变量达到第一个’\0’的字符串长度,不会包括’\0’
-
sizeof()是求括号内数据类型的长度,char a = ‘5’的数据类型是char,1个字节,char* str = "some"的数据类型是char*为指针,在64位计算机中为8,char[] str = “some” 如果将字符串数组名理解为首字符的指针那么这里就会出问题,会认为答案是8而不是5,C语言标准规定,当数组名作为数组定义的标识符(也就是定义或声明数组)时,sizeof 或 & 的操作数时,它才表示整个数组本身,在其他的表达式中,数组名会被转换为指向第 0 个元素的指针(地址),所以当它在表示数组本身时候,需要求数组大小,如果为字符数组当然是包括’\0’
char str1[10] = "123456"; //此时的数据类型可以认为是 char[10] = 10 char str[] = "123456" //char[7] = 7 int arr[] = {1, 2, 3, 4} //int[4] = 16
-
函数
- strcat(str1, str2):链接字符串 str1 = str1 + str2,需要保证str1的长度
- strcpy(str1, str2):覆盖拷贝字符串,str2 -> str1,需要保证str1的长度
- strcmp(str1, str2):比较字符串, str1 - str2
数组越界和溢出
- 越界:修改、读取不在数组申请范围内的下标
- 溢出:初始化时候长度超过申请范围,字符串在赋值时候需要注意,如果’\0’不在范围不会被初始化,输出和strlen都会出现问题
函数
变量
-
全局变量:函数外变量,加static作用域为当前文件,不加作用域为整个程序
-
局部变量:函数内变量或者块内变量,如果在全局变量中有同名变量,优先使用局部变量
- 块内变量指的是,{}内的变量,如if,while等,c语言也允许单独的代码块出现
-
const:指const修饰的部分不能改变,去掉变量类型,就能看到const修饰的部分
-
例子:
const int* a; //-> const *a 此时修饰的是*a,故而数据不能改变 int const *a; // -> const *a 同上 int* const a; // * const a; 此时i修改的是a,故而指针不能改变 const int* const a; // const * const a; 此时修饰的*a 和 a,所以都不能改变
-
作用:如果只能禁止修改那么可以使用#define代替,const不仅能够防止程序员误操作,还能作为函数参数告诉调用者,这个函数不会通过指针改变参数值,但是虽然const int* a无法通过a来改变*a的值,但是理论上可以通过将a的值赋给别的变量,来改变 *a 的值,所以这种操作一般是被警告或者错误的,但是普通变量直接转为const变量是允许的
-
递归
结束条件 + 重复计算 + 调用自身
预处理
-
文件包含命令 #include
- <>会在系统路径下找头文件,而""会先在当前文件下找如果找不到在去找系统文件
- 不要在头文件中定义函数和全局变量,会引起重复引用的重复定义,即使使用#ifndef也不建议这样做
-
宏定义 #define
-
注意宏定义的括号问题,避免公式替代导致的运算顺序改变,全加括号简单粗暴
-
一般全大写
-
宏定义虽然也可表示数据类型
-
对带参数的宏,在展开过程中不仅要进行字符串替换,还要用实参去替换形参
#define M(y) y*y+3*y
-
区别于函数,只是简单的字符替换,不会分配内存
-
# 和 ##
//#会在参数左右加上引号 #define STR(s) #s printf(%s, STR(www.baidu.com)); //-> printf(%s, "www.baidu.com"); //##将宏参数或其他的串连接起来 #define CON1(a, b) a##e##b #define CON2(a, b) a##b##00 printf("%f\n", CON1(8.5, 2)); //-> printf("%f\n", 8.5e2); printf("%d\n", CON2(12, 34)); //-> printf(%d\n"", 123400)
-
预定义宏
__LINE__ 源代码行号,__FILE__ 源文件名称,__DATE__ 编译日期,__TIME__ 编译时间
-
条件编译
这种能够根据不同情况编译不同代码、产生不同目标文件的机制
#if __WIN64__ printf("%s len1=%I64u size=%I64u\n%s len2=%I64u size=%I64u\n%s len3=%I64u size=%I64u\n%s len4=%I64u size=%I64u\n", str1, strlen(str1), sizeof(str1), str2, strlen(str2), sizeof(str2), str3, strlen(str3), sizeof(str3), str4, strlen(str4), sizeof(str4)); printf("arrF size is %I64u\n", sizeof(arrF)); #elif //... #else printf("%s len1=%llu size=%llu\n%s len2=%llu size=%llu\n%s len3=%llu size=%llu\n%s len4=%llu size=%llu\n", str1, strlen(str1), sizeof(str1), str2, strlen(str2), sizeof(str1), str3, strlen(str3), sizeof(str1), str4, strlen(str4), sizeof(str1)); printf("arrF size is %llu\n", sizeof(arrF)); #endif
#ifndef FUNCTION_H #define FUNCTION_H class Function { //... }; #endif // FUNCTION_H
-
阻止编译 #error
#ifndef __cplusplus #error 当前程序必须以C++方式编译 #endif
-
其他
-
#undef 取消已经定义的宏
-
#ifdef 如果宏已经定义
-
#if 后面跟的是整型常量表达式,而 #ifdef 和 #ifndef 后面跟的只能是一个宏名,#ifdef 可以认为是 #if defined 的缩写
#define NUM1 10 #define NUM2 20 //... #if (defined NUM1 && defined NUM2) printf("NUM1: %d, NUM2: %d\n", NUM1, NUM2); #endif //...
-
-
指针
指针初始化
指针的初始化需要用地址,比如 int* a 的初始化,需要int* 类型的地址,常用的返回地址的方式有&、malloc,如果暂时没有空间分配,可以使用NULL初始化,最好不要直接 int* a; 结束,需要使用NULL初始化内存,即((void *)0),避免各种未知错误
指针函数
返回值是指针的函数,局部变量会在函数被销毁时销毁,所以不能将函数内部定义的变量的指针返回,一般返回参数的指针参数
void指针
表示指针指向的数据的类型是未知的,malloc()的默认返回值就是void*类型,可以直接强制转化
数组和指针
-
通过sizeof关键字求类型大小,认为数组的数据类型是带有长度信息的,例如int arr[10]的数据类型为int[10],参考
-
数组名有时候会转换为指向数据集合的指针(地址),而不是表示数据集合本身,C语言标准规定,当数组名作为数组定义的标识符(也就是定义或声明数组时)、sizeof 或 & 的操作数时,它才表示整个数组本身,在其他的表达式中,数组名会被转换为指向第 0 个元素的指针(地址)
-
C语言标准规定,数组下标与指针的偏移量相同,也就是先有的偏移量再有的下标标识
-
C语言标准规定,作为“类型的数组”的形参应该调整为“类型的指针”,也就说在函数参数传递之后,即使是sizeof也不能表现为数组性
-
注意多维组数和多级指针的转换
int arr[3][3][3] = {{{1, 2, 3, 4, 5}, ... , 125}}}; printf("arrCube[1][2][3] = %d, *(*(*(arrCube+1)+2)+3) = %d", arrCube[1][2][3], *(*(*(arrCube+1)+2)+3)); //arrCube[1][2][3] = 39, *(*(*(arrCube+1)+2)+3) = 39
函数指针
指针变量指向函数所在的内存区域,然后通过指针变量就可以找到并调用该函数
//returnType (*pointerName)(param list);
int max(int a, int b){
return a>b ? a : b;
}
int main() {
int x, y, maxval;
//定义函数指针
int (*pmax)(int, int) = max;
printf("Input two numbers:");
scanf("%d %d", &x, &y);
maxval = (*pmax)(x, y);
printf("Max value: %d\n", maxval);
return 0;
}
将指针赋予函数之后,函数拥有了变量的特性,可以被作为参数,可以被作为map的value,可以被作为枚举,让数据结构更为灵活C++继承实现的多态归根结底还是对实际对象所绑定的方法的调用,继承不是实现运行时多态的唯一途径,函数指针一样可以做到。并且可以实现接口分离,比如std::sort提供函数作为比较参数
void ptrFuncAssistant1() {
printf("run in ptr function assistant1\n");
}
void ptrFuncAssistant2() {
printf("run in ptr function assistant2\n");
}
void Function::ptrFunc() {
void (*ptrFunc)(void) = ptrFuncAssistant1;
(*ptrFunc)();
void (*ptrFuncArr[])(void) = {ptrFuncAssistant1, ptrFuncAssistant2};
0[ptrFuncArr]();
(*ptrFuncArr[1])();
}
//http://c.biancheng.net/view/vip_2024.html
int (*(*(*pfunc)(int *))[5])(int *);
main函数参数
int main(int argv, char* argv[]) {
return 0;
}
argc是输入命令参数,argv是参数的指针数组,参数包含执行命令exe,所以argv至少有一个元素,argc至少为1
结构体
定义一种新的数据类型用于数据处理,初始化类似于数组,结构体是创建变量的模板,不占用内存空间;结构体变量才包含了实实在在的数据,需要内存空间来存储
struct stu{
char *name;
int num;
};
stu stu1 = {"zs", 97};
stu stuArr[] = {{"ls", 100}, {"ww", 77}};
和其他数据类型一样,结构体可以同行定义多个变量,定义函数、指针,也可以在定义的时候直接初始化,直接认为是长一点的普通类型就可以,结构体定义写在函数内外都可以,结构体的用处是为了复用,所以一般不写在函数内
枚举类型
//默认首位为0,依次递增
enum weekEnum {MON=1, TUES, WED, THURS, FRI, SAT, SUN};
weekEnum mon = MON:
和结构体一样,当作普通变量看待,但是右值被限定在枚举当中,c语言中枚举的左值可以理解为int,右值和宏相似,宏是在预编译阶段替换的,可以将枚举的右值替换理解为编译阶段的宏,在编译的某个阶段,枚举会被替换成数字,也就是它们不占用数据区(常量区、全局数据区、栈区和堆区)的内存,而是直接被编译到命令里面,放到代码区,所以不能用取地址符&取得它们的地址
共用体/联合体
结构体变量的各个成员会占用不同的内存,互相之间没有影响;而共用体变量的所有成员占用同一段内存,修改一个成员会影响其余所有成员。结构体变量占用的内存大于等于所有成员占用的内存的总和(成员之间可能会存在缝隙),共用体变量占用的内存等于最长的成员占用的内存
基于以上特性,联合体不能像结构体一样初始化,只能初始化单个成员
union tea {
char firstCh;
int age;
};
tea monster;
monster.age = 0x123456;
//56,大端方式会为12
printf("%x\n", monster.firstCh);
通过联合体可以判断大端和小端
大端和小端
大端和小端就是优先处理数据的哪个部分,小端的就是数据的低位,大端就是高位,计算机计算的时候从低位算更简单,但是人类还是习惯大端字序,所以现在pc大部分都是小端字序。对于计算机来说,优先处理即地址更低,所以小端是低地址放低位
计算机选择小端优势:计算方便,包括强制不需要调整字节的内容,低位到高位的算据计算以及最后的刷新符号位
计算机选择大端优势:符号位在前,方便快速判断数据大小/正负
位域
在结构体定义时,我们可以指定某个成员变量所占用的二进制位数bit,C语言标准规定,位域的宽度不能超过它所依附的数据类型的长度,官方规定允许的依附类型只有int和unsign int但是编译器有拓展
struct scr {
unsigned int bitRange: 8;
};
void Function::numIO()
{
scr srcBit;
srcBit.bitRange = 0b101010;
char bitString1[10] = {0};
//stdlib.h 输出进制字符串到字符串
itoa(srcBit.bitRange, bitString1, 2);
printf("%s\n", bitString1);
//00001010 11101010
srcBit.bitRange = 0b0000101011101010;
char bitString2[10] = {0};
itoa(srcBit.bitRange, bitString2, 2);
printf("%s\n", bitString2);
}
同样,用位域也可以判断大端和小端。位域的存储不同编译器处理不一样,但是都在压缩空间,一般不遵从struct的寻址效率优先的原则,位域的存储
无名位域:一般用来调整位置,拒绝压缩
struct bs{
int m: 12;
int : 20; //该位域成员不能使用
int n: 4;
};
位操作
运算符 | & | | | ^ | ~ | << | >> |
---|---|---|---|---|---|---|
说明 | 与 | 或 | 异或 | 非 | 左移 | 右移 |
- 异或:是否为不同
- 两次异或可以用于加密
- 左移和右移是在整个范围内左右移,譬如32位中11110000左移1位为111100000,但是8位中11110000左移一位为11100000
typedef
用于取别名
typedef int INTERGER;
//int zz-> INTERGER zz
typedef struct stu {
char name[20];
int age;
char sex;
} STU;
//struct stu zs-> STU zs
typedef int (*PTR_TO_ARR)[4];
//int arr[][4] -> PTR_TOARR arr
typedef int (*PTR_TO_FUNC)(int, int);
//int (*pmax)(int, int) -> PTR_TO_FUNC pmax
区别于#define,定义上#define定义量不加分号,typedef在后加分号,实质上,#define只是简单的替换,而typedef是实在的类型变换,表现在:
- 当 typedef int* INTERGERPTR; 以及 #define INTERGERPTR int* 时,对于 INTERGER a,b 的解释不同
- 当替换时候如果是int可以加unsigned修改类型,但是如果已经定义了typedef就不能通过前加unsigned来改变类型了
文件操作(跳过)
调试
简单的异常处理:assert(false),表达式为false直接抛出
VS调试的功能:断点、调用堆栈、查看局部变量值、修改局部变量值、监视变量(右键变量)、逐过程运行、逐语句运行、跳过代码位置(拖动“下一条”指针)、即时窗口(利用当前断点所在区域能获取的变量和函数进行命令解释)、内存查看、断点条件(右击断点选择停止条件,不在每次经过断点时停止)
内存
-
内存对齐:内存对齐不仅仅出现在结构体当中,正常变量也会进行内存对齐,将一个数据尽量放在一个步长之内,避免跨步长存储,这称为内存对齐
-
结构体中的内存对齐的逻辑:按照变量依次往下排,每个变量都排在该变量的长度的整数倍的地址上,比如int变量只能排在0X00、0X04、0X08这样的地址上,排完之后由最长类型向上取整数倍,比如排完之后是20,最长类型是long long int,那么实际大小为24,当结构体中含有结构体时,内含的结构体排在该结构体内最长类型的整数倍的地址上,如某个结构体中含有int*变量,那么那在64计算机中含有该结构体的结构体在排该结构体时,只能排在0X00、0X08的位置上,并且最长类型会在包含内部结构中的类型在内的所有类型中选择进行向上取整的长度补正
-
多级页表的虚拟内存机制:将虚拟地址的32位/64位分为几段,每多一段代表多一级页表,最后一段用于定位实际地址的偏移量,向前的每一段页号都可以通过页表寄存器转换成上级页表所在地址,从而找到本级页号/物理地址前缀(前段存放本级页号,后段可以用来表示当前页的相关属性,例如是否有读写权限、是否已经分配物理内存、是否被换出到硬盘等)
-
程序内存区域:
- 程序代码区:存放函数体的二进制代码。一个C语言程序由多个函数构成,C语言程序的执行就是函数之间的相互调用。
- 常量区:存放一般的常量、字符串常量等。这块内存只有读取权限,没有写入权限,因此它们的值在程序运行期间不能改变。
- 全局数据区:存放全局变量、静态变量等。这块内存有读写权限,因此它们的值在程序运行期间可以任意改变。
- 堆区:一般由程序员分配和释放,若程序员不释放,程序运行结束时由操作系统回收。
- 栈区:存放函数的参数值、局部变量的值等,其操作方式类似于数据结构中的栈。
- 动态链接库:用于在程序运行期间加载和卸载动态链接库。
- 程序代码区、常量区、全局数据区在程序加载到内存后就分配好了,并且在程序运行期间一直存在,不能销毁也不能增加(大小已被固定),只能等到程序运行结束后由操作系统收回,所以全局变量、字符串常量等在程序的任何地方都能访问,因为它们的内存一直都在。
-
函数调用和栈:
-
函数被调用时,会将参数、局部变量、返回地址等与函数相关的信息压入栈中,函数执行结束后,这些信息都将被销毁。所以局部变量、参数只在当前函数中有效,不能传递到函数外部,因为它们的内存不在了,对每个程序来说,栈能使用的内存是有限的,一般是 1M~8M,这在编译时就已经决定了,程序运行期间不能再改变。如果程序使用的栈内存超出最大值,就会发生栈溢出(Stack Overflow)错误,通过参数来修改栈内存的大小
-
函数出入栈是有不同规定的,比如函数参数可以按照从右到左的顺序入栈,也可以按照从右到左的顺序入栈
//_cdecl 在vs中表示按照从右到左的顺序入栈 int __cdecl max(int m, int n){ int max = m>n ? m : n; return max; } //_attribute__((cdecl)) 在gcc中表示从右到左入栈 int _attribute__((cdecl)) max(int m, int n){ int max = m>n ? m : n; return max; }
-
在函数入栈的过程中,debug模式生成程序,会留出多余的内存,方便加入调试信息;以Release模式生成程序时,内存将会变得更加紧凑,空白也被消除。
-
栈溢出:当输入字符过多时候,参数所在内存发生溢出,占用其他数据内存,比如返回地址内存,并将原有的数据覆盖,这样函数执行完获得错误返回地址,故而出错,C语言不会对数组溢出做检测,这是一个典型的由于数组溢出导致覆盖了函数返回地址的例子,我们将这样的错误称为“栈溢出错误”,如果用户故意让返回地址指向恶意代码,那就比较危险了,这就是常说的栈溢出攻击
-
-
动态内存分配:
代码区、常量区、全局数据区的内存在程序启动时就已经分配好了,它们大小固定,不能由程序员分配和释放,只能等到程序运行结束由操作系统回收,这称为静态内存分配。栈区和堆区的内存在程序运行期间可以根据实际需求来分配和释放,不用在程序刚启动时就备足所有内存。这称为动态内存分配。
void* malloc (size_t size); void free(void* ptr); //example int *ip; //sizeof 是一个单目操作符,不是函数 if((ip = (int*)malloc(N * sizeof(int))) == NULL) { printf("memory allocated failed!\n"); exit(1); } for(int i = 0; i < N; i++) { ip[i] = i; printf("ip[%d] = %d\t", i, ip[i]); } free(ip); ip = NULL: //重新改写ptr的内存大小,使用后之前的指针被系统回收,不需要处理之前的指针,之前的内存区域里的值也会被完全覆盖到新的内存区域 void* realloc(void *ptr, size_t size);
free§ 并不能改变指针 p 的值,p 依然指向以前的内存,为了防止再次使用该内存,建议将p的值手动置为 NULL
-
内存池
栈内存的分配类似于数据结构中的栈,而堆内存的分配却类似于数据结构中的链表,堆内存对于内存区的占用是不连续需要利用链表连接,之后又发展成内存池,最终目的都是减少碎片化,高效利用内存空间,详见《计算机操作系统》
-
野指针
指针指向的内存没有访问权限,或者指向一块已经释放掉的内存,那么就无法对该指针进行操作,这样的指针称为野指针,在GCC下会提示段错误,
规避野指针:
- 使用指针前:如果暂时不需要申请空间赋值,则给NULL
- 使用指针后:不仅需要free,也需要置为NULL
-
内存泄漏
申请的内存区域没有释放,或者改内存区域的指针值被覆盖无法找到,那么这块内存就被浪费了,无法操作
-
存储类别
我们可以通过关键字来控制变量的存放区域,共有4个关键字用来指明变量的存储类别:auto、static、register、extern
- auto,自动,加不加没啥区别
- static,声明的变量称为静态变量,不管它是全局的还是局部的,都存储在静态数据区,全局变量即使不加static也在静态数据区
- register,将变量放入寄存器而不是内存,用该变量时就不必访问内存,直接从寄存器中读取,大大提高程序的运行效率,只有局部变量和形式参数才能定义为寄存器变量,CPU的寄存器数目有限,即使定义了寄存器变量,编译器可能并不真正为其分配寄存器,有的编译器能自动识别使用频繁的变量,如循环控制变量等,在有可用的寄存器时,即使没有使用 register 关键字,也自动为其分配寄存器,无须由程序员来指定,所以这个关键字基本用处不大
- extern,声明,函数声明可以不用加extern,没有函数体即为声明,但是变量在声明时候一定要加extern,没有extern就是定义,可以在声明时候初始化但是不推荐这样做,而且会警告或者报错
模块化开发
程序运行过程
预处理 -> 编译 -> 汇编 -> 链接
链接
程序运行之前确定符号地址的过程叫做静态链接;如果需要等到程序运行期间再确定符号地址,就叫做动态链接。Windows下的.dll或者Linux下的.so必须要嵌入到可执行程序、作为可执行程序的一部分运行,它们所包含的符号的地址就是在程序运行期间确定的,所以称为动态链接库。链接的一项重要任务就是确定函数和全局变量的地址,并对每一个重定位入口进行修正
头文件
#include是一个宏命令,作用于程序执行的预处理阶段,其功能是将它后面所写文件中的内容,完完整整、一字不差地拷贝到当前文件中
一般规则:可以声明函数,但不可以定义函数。可以声明变量,但不可以定义变量。可以定义宏,包括带参的宏和不带参的宏。结构体的定义、自定义数据类型一般也放在头文件中。
外部方法的引入:一般不直接使用外部源代码而是引入外部头文件,然后链接上对应的静态库
路径:使用尖括号,编译器会到系统路径下查找头文件;而使用双引号,编译器首先在当前目录下查找头文件,如果没有找到,再到系统路径下查找。引入头文件时候可以选择绝对路径和相对路径,尖括号会相对系统路径,双引号会优先相对当前路径
防止被重复包含:
//兼顾效率和兼容性
#pragma once
#ifndef _STUDENT_H
#define _STUDENT_H
class Student {
//......
};
#endif
static
- 隐藏:程序有多个模块时,将全局变量或函数的作用范围限制在当前模块,对其他模块隐藏。当修饰对象为全局变量时,这个性质起效,因为局部变量本身对于其他文件就是隐藏的
- 保持变量内容的持久化:将局部变量存储到全局数据区,使它不会随着函数调用结束而被销毁。当修饰对象为局部变量时,这个性质起效,因为全局变量本身就存储在全局数据区。**全局数据区的变量只能被初始化(定义)一次,以后只能改变它的值,不能再被初始化,即使有这样的语句,也无效,**所以局部变量中static的重复定义不会被识别
Tips
- 全局变量即使不赋值也默认为0,局部变量根据编译器的不同做不同的处理或者不处理
- void *malloc(size_t size),是返回所申请空间的指针,所以如果先定义一个指针然后再malloc指针的地址是会变化的,不是在原地址空间上申请空间
- 输入是比较重要的,所有平台表现一致scanf()、getchar()、gets()在所有平台的缓冲功能都是相同的,但是由于windows由于功能的多样化,比如游戏等,需要对于输入反应灵敏,故而拥有特有的函数getche()和getch()都是不带缓冲区的。
- scanf对空格不敏感,有就行数量无所谓,但是字符串以空间作为结尾,所以语言类字符串不能使用scanf%s输入
- 换行符会进入缓冲区,但是并不会被读取,会被忽视,比如%s后接getchar,用回车结束输入字符串,那么getchar得到的是’\n’
- 读取失败不会影响当前缓冲区的位置指针,只有读取成功会,也就说,如果 (“%d %d %d”, &a, &b, &c) 中输入 1 a 1 第二个%d读取失败,第三个%d的仍然读取a,而不是接着读取错误a的后面的位置读取1,所以如果之后还有代码取 %d 那么也会一起错误,直到取到 %c 或者 %s 为止,缓冲区一直放着错误的数据,此时应该使用 while((c = getchar()) != ‘\n’ && c != EOF); 等手段清除缓冲区数据
- 对空格不敏感的是scanf中的控制符%,而不是scanf,也就是说如果scanf(“AA%s”),如果缓冲区中的内容为"\nAAstring",那么则读取失败
- Linux、Mac中是有输出缓冲区的,"\n"和fflush(stdout)命令可以刷新缓冲,输出行缓冲区的内容,但是windows没有输出缓冲区