c语言精粹02

c语言精粹02

0x00 字符串输入

​ ①首先看scanf(“%s”,str) 这个输入,一旦碰到空白或者换行符,就以’\0’结束,不包含空白与换行符,但是也不丢弃,也就是空白和换行仍然在输入缓冲区里,待读取。同时,因为不检查输入长度,str可能会溢出,溢出最大的问题不是程序终止(修改了栈帧的ret,导致返回到错误地址 一般segment fault),而是被执行恶意代码。

​ ②再看gets(),gets只碰到换行符才结束输入,同样输入不包含结尾的换行符,而是会将换行符变为’\0’作为结束。但是,gets()会从输入缓冲区丢弃换行符,同样,会溢出,不安全。

​ ③fgets(char*buf,size_t n,file * __stream),则比较奇葩,它可以限制读入字符,保证不溢出,同样时以换行符或者EOF结束输入(缓冲区空),但是会读入换行符

0x01 字符串函数

​ 都在string.h里

strlen(char*);//返回字符串长度

strcat(char* str1,char* str2); //将str2复制到str1结尾,可能会溢出
strncat(char* str1,char *str2,size_t n);//长度限制的strcat

strcmp(char* str1,char* str2);//ascill顺序比较,相等返回0
strncmp(char* str1,char* str2,size_t n);//长度限制的strcmp

strcpy(char* dst,const char* src);//从src复制到dst 可能溢出
strncpy;//限制长度的复制

sprintf(char* str,const char* format,...);//可变参数的 字符串格式化 有溢出风险
snprintf(char* str,size_t n,const char* format,...);//限制长度的字符串格式化

0x02 变量的本质

​ 变量就是可变的量,储存在内存里,那么变量的名字(标识符),就是内存地址的别名。声明一个变量(extern int a),并不分配内存,而定义一个变量,才分配内存(int a)。

extern int a;//declaration
int a;//definition

​ 能在赋值=左边的,称为左值

0x03 变量的生命周期、作用域、链接标识符

​ 变量在内存的存活时间,变量名能够代表对应内存的期间。

​ 作用域,即能够访问到该变量名的区域。

​ 链接标识符,即在多文件链接时,该标识符是仅在源文件本地可见,还是全局可见

0x03 生命周期

​ 静态储存期:即整个程序运行期间都存在,全局变量和static 前缀的静态变量

​ 线程储存期:并发编程时

​ 自动储存期:块级作用域的变量都是

​ 动态分配储存器:自己free

0x04 作用域

​ 首先,块级作用域,由{ } 花括号表示,在花括号内定义的非静态变量,都是局部,包括函数,都只能在对应的括号结束前访问。

​ 其次对于定义在函数外面的变量,非静态的,都是全局变量,不仅可以在本文件的源码里访问,也可以在链接后,由其它文件的源码访问,但是其他文件要用extern声明有这个变量,不然无法使用。

​ 最后,对于所有的静态的变量,不论何处定义,都是只在文件内可见,链接时其他源文件看不见,即使声明了,也无法访问。

0x05 变量的隐式初始化

​ c语言中所有的基础变量,包括由基础变量定义的结构体,都存在以下的初始化机制:

​ ①全局的,或者静态的变量,如果没有显式初始化,那么就为0,为0的变量,编译后,都在可执行文件(elf或者pe)的bss section,不占用实际大小。只在load到内存时,才分配被清0后的内存,所以值为0。(如果显式初始化,则在.data section ,最终两者合并data segment)

​ ②局部的非静态的变量,没有显式初始化,则值不确定,因为是保存在栈中(也不一定,如果没有访问其地址,很可能会使用寄存器保存),栈从来不主动清0

0x06 多文件链接与头文件

​ ①首先说一下为什么需要头文件,对于一些多个文件共用的声明,宏,类型声明等声明或者只在文件内可见的定义,我们不想在每个.c文件里都写一次,因此,写在.h文件里,然后用到时include即可。

​ ②include头文件的实质,是编译时预处理,直接将.h里的文本添加.c里。

​ ③头文件重复引用是怎么回事?如果1.h引入了2.h 1.c里既include 1.h,又include 2.h 那么实际上2.h里的内容被引入了两边,因此2.h里的如果有定义,就会重复(比如struct的重复定义)。一般用ifndef define endif 或者#program once来解决。

​ ④头文件里能放什么?由于头文件最终被引入到多个.c源文件,如果这写.c源文件要链接,那么头文件里的内容,要么只能是声明(如函数、extern变量及结构体声明)(可以允许多次声明),要么是只在文件内可见的定义,如static 的变量(实测gcc中未初始化的全局变量也是仅文件内可见,虽然readelf里显示符号为global)

0x07 前缀

const 不能修改的

volatile 易变的,要求编译器让此变量每次都读写内存而不是缓存cache

​ 可以有const volatile 即不能直接修改,但是通过指针运算,可以间接修改

_Atomic 原子的,cpu最小执行单位是指令,而不是c的语句,因此对于一个变量,特别是结构体变量的读写,不是原子操作,可以使用 _Atomic强制原子操作,不让其他线程打断

0x08 系统io与标准io

​ linux系统提供open close write read lseek等一串io操作的系统调用,是非常底层的,非常通用原始的操作。(unistd.h)

​ 而c语言标准库中的,则提供了一套基于系统io调用的封装函数。

​ 系统io是只有二进制的读写,直接访问内核读写缓冲区的函数,而标准io则区分文本与二进制,并且在用户层做了缓冲减少访问内核的次数,提高效率

file* fopen(const char* pathname,const char* mode);//按照模式打开文件 
fread()//二进制读写 file*
fclose()
    
getc() putc()//读写文本file* 一个字符
fprintf() fscanf() //读写文本file* 
fgets()
fputs()
    
fseek() ftell() //前者设置读写指针位置 后者获取 类型是long
fsetpos() fgetpos() //一样的功能,但是定义了新的类型,设置读取的范围比long大

fflush() //立即刷新缓冲区,将数据写入内核缓冲区
setvbuf()//使用提供的缓冲区代替默认的缓冲区,可以设置缓冲模式
    //(完全缓冲:满了才刷新   行缓冲:换行符刷新  无缓冲)

​ 每个进程,系统io都默认有文件描述符 0 1 2 (err 输入 输出),因此标准io对应有stderr stdin stdout 三个file*

0x09 结构体的列表初始化和初始化器

​ 和数组一样,结构体也支持列表初始化与初始化器,同样的 列表初始化时,未指定的成员默认为0

struct A{
    int a;
    int b;
    int c;
};
struct A ex={1,2,3}  //ex.a=1 ex.b=2 ex.c=3   
struct A ex1={.c=3}  //ex.a ex.b没有指定 为0  ex.c是3

0x0a 嵌套结构与结构体指针

​ A结构中,B结构的变量作为成员,即为嵌套,但是如果B结构中又有A那么不行,很容易理解。

​ 但是指针不一样,指针永远是4字节或8字节的地址而已,因此A结构中有B结构 B结构中有A的指针,是可以的。

​ 再简单一点,考虑A结构中有A结构的指针,也是可行的。(链表)

0x0b 结构作为实参

​ 与数组不一样,c不支持数组的赋值,因此数组作为参数时传递的是首地址。而结构不一样,结构可以赋值(深拷贝,嵌套结构一起拷贝),因此结构可以作为参数传递,同时也可以使用其指针作为参数,这样方便修改。

0x0c 匿名结构

struct{
    int a;
    int b;
} once;

​ 考虑上面这个写法,发现struct声明居然没有名字!这是可以的,这样的话,无法再定义这个struct类型的变量了,但是它本身在声明时就定义了一个once变量。 嵌套结构里也有这么用的

struct person{
    int id;
    struct {char first[20];char last[20];};//匿名结构
}

0x0d 结构体大小

​ 结构体的大小,实际上与内存对齐有关,首先说说什么是内存对齐:内存对齐是计算机系统对某些基本数据类型的合法地址做出的一些限制,要求地址是某个值K的整数倍,方便硬件设计,具体K值如下:

char 1
short 2
int float 4
long double 8
//指针视情况 32 64位

​ 并且,结构体声明中,在前面的成员处于低地址,变量的起始地址总是4字节或者8字节对齐的(32/64位),因此成员的位置对结构体的大小(sizeof得到的)有影响:

struct A{
    char a;
    int b;
} instance1;
sizeof(instance1) //8 bytes  b的地址被对齐了(也就是a和b之前有空)
struct B{
    int b;
    char a;
} instance2;
sizeof(instance2) //5 bytes 都满足对齐,size就是5

0x0e 联合union

联合是一个地址空间,允许看作多种类型,其占用地址空间的大小,总是最大的那个成员的大小

union A{
    int a;
    char b;
    double c;
} instance;
sizeof(instance)//8字节
instance.c=1.0//那么instance.a instance.b也变了

0x0f 枚举

枚举类型实际上是整型变量的集合

#define white 0
#define red 1
#define blue 2
#define green 3
//等价于
enum Color{white,red,blue,green};
Color a=red;//a=1

​ 默认情况下,枚举类型中的常量,是0开始 递增,但是也可以手动赋值

enum Num{low=100,medium,hight};//medium递增,hight递增