工作五年C语言知识点整理
1 const
1.1 const 在*左边
const 在*左边, 比如const int *p 或 int const *p,指针指向的值的内容不能变,即p可以任意指向一地址p=&a,但是*p无法更改;
举例,以下函数,防止入参num被子函数修改。
cJson_CreatIntArray( const int * num, int count);
(2019.2.14)
1.2 const 在*右边
const 在*右边,比如 int * const p3=&a,指针地址不能变,所以必须初始化,*p3可以任意赋值。
(记忆方法:*p是否为整体,若是所代表的值不能被更改;若不是,指针必须初始化,不能另指)
1.3 const修饰返回值
const修饰返回值时,其返回值内容不能被修改,只能复制给加const修饰的同类型指针。
const char *str = cJson_GetErrorPtr();
2 static
2.1 修饰函数局部变量
修饰函数局部变量,开辟内存;
2.2 修饰全局函数和全局变量
修饰全局函数和全局变量——》》只能在本源文件使用,即便extern外部声明也不行;
3 指针
3.1 双重指针
char **p = "123";
上记表达式的解释如下:
地址 | 值 |
---|---|
p = 0x81 | *p = *(0x81) = 0x82 |
*p = 0x82 | *(*p) = *(0x82) = “123” |
3.2 函数指针
以下是函数指针应用的一个例子,注意TBL1和TBL_p是两个层次上的数组,合起来其实是一个二维数组。
typedef struct{
int num;
int* fun(void*); /*结构体中的函数指针,即函数名,其代表函数的入口地址*/
}STR;
STR TBL1[] = /*结构体数组,数组名本身是指针*/
{1, FUN1},
{2, FUN2},
{0, NULL}, /*此行末尾可以加逗号,但不建议*/
};
STR TBL2[] =
{1, FUN3},
{2, FUN1},
{0, NULL},
};
STR* TBL_p[] = {TBL1,TBL2,NULL}; /*结构体数组名(指针)作为成员的一维指针数组*/
RunTbl_p = TBL_p[0];
ret = RunCtrl(RunTbl_p); /*此函数逻辑上对TBL1上的函数依次执行,也可根据执行情况,跳着执行*/
(2020.5.21)
4 字符char
4.1 “字符型数据类型”和“字符”
注意“字符型数据类型”和“字符”的区别,char 是可以存储数字的;
字符型char是整数类型,只是为了方便处理字符,通过ASCII码用特定整数表示特定字符。
例如
char a = 48;
对于内存而言是相同的,输出使编译器会选择相应形式。
输出形式 | 值 |
---|---|
%d | 48 |
%c | ‘0’ |
(2019.3)
4.2 常用字符ASCII码
字符 | ASCII |
---|---|
空字符 ‘\0’ | 0 |
空格字符 | 32 |
‘0’ | 48 |
4.3 字符串
C 语言中并不存在字符串这个数据类型,而是把字符串当作数组来处理的,比如char *a=“hello”;a[1]=“e”;
换句话说,字符串属于char类型的数组。并且,一定要‘\0’结束,即使空字符串也是。
=>即字符串是以‘\0’结尾的特殊的char类型数组。
字符串赋值与比较要用到库函数。(初始化除外)
(2019.4.15)
4.4 NULL
NULL (void *0)值为0,其实是0地址,C语言中不允许被访问。
C程序中 NULL = ‘\0’ 为真,只是两者数值正好相同,但两者本质不同。
(2019.7.5)
5 类型长度
5.1 sizeof
操作符sizeof(int) 和 sizeof( i )均合法。
注意对于计算字符串长度sizeof和strelen的区别:
char ss[] = "abcd";//以‘\0’结尾的特殊的char类型数组=>字符串
int len1 = sizeof(ss);//为4+1=5
int len2 = strlen(ss);//4
char ss2[3] = "";//char类型数组
int len3 = sizeof(ss2);//3,注意不是字符串,所以无需加‘\0’这个字符
int len4 = strlen(ss2);//0,strlen专注于字符
(2022.11.30)
5.2 64位macine
在64位机器中各类型的byte长度如下:
类型 | 字节长度 |
---|---|
short | 2 |
int / long | 4 |
float | 4 |
double | 8 |
void * | 8 |
(2020.10)
5.3 float
注意计算机单精度浮点数float在内存中以科学计数法的形式储存,与int,char本质上不同。
6 C语言内存模型
high address | ||
---|---|---|
stack | 局部变量,函数参数 | |
heap | 动态内存分配(malloc,free) | |
BSS | 存放未初始化的全局变量与静态变量 | |
Data | 存放初始化后的全局变量和静态变量 | |
文字常量区 | 常量字符串 | |
Text | 代码 | |
low |
注意点1:stack和heap相向延申;
注意点2:常量字符串存放与文字常量区,程序结束时系统释放,比如
strcpy(p1,"123456");
"123456"放在常量区,所有”123456“都会被编译器优化成一个地方。
7 数组与结构体
7.1 直接赋值问题
数组相较结构体是C语言中的二等公民,后者可以作为函数参数和返回值,前者不行,传参时退化成一个指针。
数组不能直接赋值,因为数组名是一个地址常量,常量不能被赋值。但是当一个数组是一个结构体成员时,可通过结构体之间的赋值间接达到数组整体复制的效果,即:
stru1.array = stru2.array; /*非法*/
stru1 = stru2; /*合法*/
汇编上,结构体赋值,采用类似memcpy的形式,而不是逐字copy。(类似C++中的浅拷贝,有指针的情况下留意,free不当,容易出现野指针。)
(2019.8.2)
#include <stddef.h>
#include <stdio.h>
typedef struct{
int s1;
int s2;
}Struct;
int test()
{
return 0;
}
int test2()
{
return 0;
}
int main()
{
int array[3] = {1,2,3};
Struct stru = {1,2};
Struct sru2 = stru;
int a = 5;
void* p = NULL;
p = array;
printf("%x\n", p);
p = test;
printf("%x\n", p);
p = &stru;
printf("%x\n", p);
p = &a;
printf("%x\n", p);
return 0;
}
如上述代码所表示的,结构体名跟普通变量名一样,需要通过&来取地址,其是变量;而数组名和函数名一样本身就代表地址,是地址常量,如同&stru2 = &stru
非法一样,数组名之间也无法赋值,进一步说,array是常量,而array[0]则是变量,这也是array与&array[0]同样表示地址的原因。
7.2 字符串数组
注意数组指针与指针数组的区别:
char a[][10]; /*a为数组指针,不可变,储存一个地址(相当于2级指针)*/
char *a[]; /*指针数组,适用于指向若干字符串,字符串没有大小限制,编译器一般优化为连续储存*/
(另外,注意 *(a+1) 与 *(a[0]+1) 的区别,前者指a[1][0]
, 后者指a[0][1]
)
了解指针数组作为main函数形参的形式:
int main(int argc, char *argv[]); /* arguement count, arguement value */
int main(int argc, char **argv); /* *argv[] => argv[][] => **argv, */
一般来说,argv[0]被系统自动赋值为程序运行的全路径名,而argv[argc]为NULL;
(2019.9.18)
7.3 结构体的位域
C语言结构体的位域(bit-field)通过bit二进位的利用,来节省存储空间,例如:
struct bf{
int a:4; /* 类型一般定义为unsigned int,避免符号位的影响 */
int :0; /* 无位域名,只用于填充int类型余下28bit,不能使用 */
int b:4;
int c:2;
int d:2; /* 剩余32-8=24bit被自动填充 */
} /* 此结构体总共两个int长度 */
注意的是,C语言中结构体存储是采用对齐的方式,提高访问效率,即变量的存放地址应该能够整除变量的字节数。比如double变量应该放在能被8整除的地址上。
(2019.7.18)
(2021.7.11)补充:关于结构体的alignment问题,以4整除单位,如下例,应该为24byte长。
struct exm{
char a;
int b; //a与b之间有3byte dummy
char c;
double d; //c与d之间有7byte dummy
}
8 define
8.1 关于C函数可变参数占位符...
如printf
的实现。
传递原理:函数参数是一块连续地址,理论上只要探测到其中一个参数地址与所有参数的类型,就可推测出其他参数的位置。
实现:需包含<stdarg.h>:va_start, va_list, va_arg, va_end. 因库函数实现机理,头个参数必须确定。传参结束机制需自己实现。
8.2 关于#define Dbg(...)
可以去除定义范围所有的Dbg函数。
并且,甚至可以不加参数,在消除Dbg函数基础上,还可以消除相关变量。
通常来说,Dbg形式如下:
#if defined(DBG_PRINT)
#define DbgFDbg DbgFPrintf2
#else
#define DbgFDbg(...)
#endif
在编译阶段,可以在gcc之后加参数-D DBG_PRINT
来定义defined(DBG_PRINT)
,DbgFPrintf2
就是实体打印函数,如果不加参数,就去除定义范围所有的Dbg函数,相当于一个debug开关作用。
(2021.8.23)
8.3 #define
的用法
#if defined(DbgTxt)
#undef DbgTxt 取消宏标识符
#define DbgTxt(p1) __FILE__,(p1),0
#endif
__FILE__
,__TIME__
等是编译器预定义宏,(p1)
是参数。
大规模开发过程中,define最重要的作用就是条件编译。
#ifndef _test.h_
#define _test.h_
//中间各种定义
#endif
用来防止头文件重复编译。
注意define的作用域只限于当前C文件,而不是整个工程。
(2021.3.25)
9 汇编语言
9.1 C语言中嵌入汇编代码
C语言中嵌入汇编代码可以提高运行效率(现在的编译器优化得足够出色),实现C语言不具备的机器要用到的功能。GNU GCC( AT&T汇编语言 )格式如下:
int main()
{
int input,output,temp;
input = 2;
__asm(
"movl %0,%%eax;\n\t" /* %%eax,寄存器使用两个百分号 */
"movl %%eax,%1;\n\t" /* %1,占位符,表示使用寄存器 */
"movl %2,%%eax;\n\t"
"movl %%eax,%0;\n\t" /* 到此处为汇编语言模板部分 */
:"=m"(output),"=m"(temp) /* 输出部分,m表示内存,not register */
:"r"(input) /* 输入部分 */
:"eax"); /* 毁坏部分,表示汇编执行过程中此寄存器会被改写,提前保护 */
return 0;
}
9.2 volatile
在多线程环境下,某个变量可能被外部改写,不能将其缓存到寄存器,每次使用时需要重新获取。因为编译器没有线程概念,需要使用volatile来限制编译器进行优化。
9.3 函数调用栈结构
32位AT&T格式汇编语言可以如下生成:
gcc -o xx.s -S xx.c -m32
寄存器介绍:
ebp | 栈底寄存器 |
esp | 栈顶寄存器 |
eip | 指令寄存器,指向代码。(存储地址) |
eax | 累加寄存器,常用于函数返回值 |
ecx | 计数寄存器 |
主要指令有 pushl,popl,movl,call,ret…
函数调用栈结构如下:
高地址,栈底方向 | |
---|---|
调用者函数栈帧 | |
返回地址 | |
上一栈帧EBP | |
局部变量1 | 被调用函数的栈帧 |
局部变量2 | |
… | 低地址,栈顶方向 |
(2019.9.19)
10 内存溢出问题
例如如下函数:
strcpy(char *dest,const char *src,int n);
当n>dest串长度时,dest栈空间溢出产生崩溃异常,即出bug,堆栈缓冲区溢出有时可以被系统检测到以产生终止该过程的segmentation fault。
C/C++中容易造成缓冲区溢出的函数:strcpy(),gets(),strcat()…数组下标越界,或打印字符串时无终止符。许多病毒就是利用缓冲区溢出漏洞对操作系统进行攻击的。
(2019.6.17)
11 运算符优先级问题
一般来说,遵循加括号原则。
但偶尔也有注意不到的时候,比如遇到下例情况:
//*pp_edtinf->lteSjtCom.sjtkind = ( UCHAR )subject;
(*pp_edtinf)->lteSjtCom.sjtkind = ( UCHAR )subject;
很容易以为*
优先级很高,而忽略->
何尝不是一个运算符!联想Python中的点运算符。
(2021.9.1)
12 i++
和++i
的区别
int i = 0;
int x = i++; //输出为0,即先看到i,直接赋值
i = 0;
int y = ++i; //输出为1,即先看到++,先自增
另外++i
可以作为左值,即允许取地址&运算符获得对应的内存地址,而i++
步行:
int i = 0;
int *p1 = &(++i);
int *p2 = &(i++);//编译error
++i 和 i++的底层区别
++i,是先取 i 的地址,增加它的内容 ,然后把值放到寄存器中
i++,是先取 i 的地址,把它的值装入寄存器,然后增加内存中 i 的值
关于 ++i 是左值,而 i++ 是右值的问题
++i,返回值是 i 本身自己,是一个变量
i++,返回值是 i 之前的一个数值,是一个数,不是变量
因此 ++(i++) 这就是错误的,因为 i++ 返回的是右值,而不能 ++右值。
关于效率
i++,会产生临时变量,效率比 ++i 要低一些,因此推荐使用 ++i。
(2022.11.30)
补充1 BCD码
BCD:Binary-Coded Decimal,二进码十进数。
十进制有十个数码,理论上,至少用2的4次方16,即4位二进制码来表示。
有许多种类,如8421码,用0000-1001对应0-9。
通常用来表示小数点后的精确值,因为浮点数总是近似的。
(2019.4.2)
补充2 cJson
Json:JavaScript Object Notation.一种轻量的数据交换格式,格式如下:
{
"a": 123456,
"b": true,
"c": "aaabbbccc",
"d": [1,2,3],
"e": {
"g": false,
"h": ["1000", "2000"]
},
"f": null
}
cJson:以标准的C写的Json解释器,由cJson.h,cJson.c组成。可以将文本形式的Json解析成链表形式,之后用户可以自定义函数将链表形式转化成结构体形式,反过来也可以将自定义结构体转化为链表形式最后打印成Json文本形式。
Json为文本格式,易于人阅读及机器解析生成,功能与XML类似。