C语言学习
十.指针详解
6.有关函数指针的代码
代码1:(*(void (*)())0)();
void(*)()是函数指针类型,0是一个函数的地址
(void(*)())是强制转换
总的是调用0地址处的函数,传入参数为空
代码2:void (*signal(int, void(*)(int)))(int);
(int, void(*)(int))是函数的传参列表
signal是函数名
剩下的是函数的返回值类型
这段代码可以简化为:
typedef void(*pfun_t)(int); //命名一个函数返回类型
pfun_t signal(int,pfun_t);
![请添加图片描述](https://img-blog.csdnimg.cn/direct/3c6f08b356c74b1589775d6b351c2214.png)
以上四种输出都等价
7.函数指针数组
定义一个函数指针数组:int (*parr[4])(int, int) = {Add, Sub, Mul, Div};
函数指针数组的作用:转移表
int(*(*ppfArr) [4])(int, int) = \pfArr;,ppfArr是一个数组指针,指针指向的数组有4个元素,每个元素的类型是一个函数指针int(*)(int, int)
8.回调函数
定义:回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们说这是回调函数
例子:stdlib中的qsort函数,qsort是一个库函数,内部用函数指针调用了一个函数
语法:qsort(数组起始地址, 数组长度, sizeof元素, 函数指针(自定义的比较方法))
比较方法的例子:
int compare(const void* e1, const void* e2){
return *(int*)e1 - *(int*)e2;
}
当返回的值小于0时,表示第一个元素应该排在第二个元素之前,所以上述代码是升序
其中:void*类型的指针可以接收任意类型的地址,但是void*类型的指针不能进行解引用操作和加减整数的操作
因为不能进行解引用操作,所以想要拿到e1中的数据就要先强转换再解引用
注意:compare函数传入的是指针,如果是结构体,调用元素要用->而不是.
指针题目
①int a[] = { 1, 2, 3, 4 };
sizeof(a+0):a是首元素地址,得到的是首元素的大小
sizeof(&a):&a取出的是数组的地址,但是数组的地址也是地址,也就是4/8个字节
sizeof(*&a):&a取得是数组的地址,*解的是数组的地址,所以得到的是16
②char a[] = { ‘a’, ‘b’, ‘c’, ‘d’, ‘e’, ‘f’};
strlen(arr):字符串数组以\0为结尾,a中没有,所以结果是随机值
strlen(arr+0):也是随机值,理由同上
strlen(*arr):会报错,字符存储时存储的是ASCII码,传给strlen的是a的ASCII码,所以会报错
③char a[] = “abcdef”;
sizeof(arr):sizeof计算的是数组的大小,所以是7
sizeof(arr+0):4/8,计算的是地址的大小
④char *p = “abcdef”;(p存储的是a的地址)
sizeof(*p):1,*p就是字符串的第一个字符
sizeof(p[0]):p[0] == *(p+0),所以是1
⑤int a[3][4] = { 0 };
sizeof(a + 1):首元素是第一行,a+1是第二行的地址,所以是4
sizeof(a[3]):不会报错,因为sizeof不会真的访问第四行,只会得到a[3]的类型,所以是16
⑥int a[5] = { 1, 2, 3, 4, 5 };
int *ptr = (int *)(&a + 1);
*(ptr - 1):a是数组的地址,a+1得到一个地址后再减一就是5的地址
⑦
p + 0x1:因为Test类型的变量大小是20字节,转为16进制后是14,所以是0x00100014
(unsigned long)p + 0x1:强转为整型后p+0x1相当于p+1,整型大小为4个字节,结果为0x00100004
其中:(0, 1)等都是逗号表达式,取最后一个数字
所以p[0] == 1
首先会报一个警告,因为p的长度和a的长度不一样
p[4][2]等价于*(*(p+4)+2),所以p[4][2]就是a[3][3]
地址相减是-4,但是"%p"是把-4的补码的直接值打印出来,所以是0xFFFFFFFC
a是字符指针的数组,存放着三个字符串的首元素的地址
pa是a数组的地址,也就是首元素的地址
pa++使其指向第二个元素,所以输出的是at
unsigned char的范围是0-255,a+b超过范围,c会变成44
str1和str2的地址不同,所以不等于
str3和str4都指向一个常量字符串,所以地址相等
十一.字符函数和字符串函数
大多字符串函数都要引入头文件:<string.h>
1.strcat
语法:char *strcat(str1, str2);
作用;将str2追加到str1后面,返回一个指向结果字符串的指针
缺点:不能将自己添加到自己后面
注意事项:
- str2中必须包含\0
- str1必须足够大
- str1必须可修改
2.strncat
语法:char *strncat(str1, str2, size);
size是要追加的最大字符串
作用:将str2的size个字符追加到str1后面,返回一个指向结果字符串的指针
3.strstr
注意:该函数要引入的头文件是<stdio.h>而不是<string.h>
语法:char *strstr(str1, str2);
作用:在str1中查找str2的第一次出现位置。该函数返回指向找到的子字符串的指针,如果未找到,则返回NULL
4.strlen
语法:size_t strlen(const char* str);
作用:返回一个字符串从头到\0的字符的个数,\0不算进长度
注意:size_t就是一个被重命名的unsigned int,无符号数字永远大于0,所以size_t跟size_t相减的结果也只会是正数
5.strcpy
语法:char *strcpy(str1, str2);
作用:将str2中的字符按顺序一个一个替换str1中的字符,注意str2中的\0也会替换一个字符,返回结果字符串的指针
注意事项:
- str2中必须有\0
- str1必须足够大
- str1必须可修改
6.strncpy
语法:char *strcpy(str1, str2, size);
size是要拷贝的字符串的最大长度
将str2中的字符按顺序一个一个替换str1中的字符,替换size次注意str2中的\0也会替换一个字符,返回结果字符串的指针
7.strcmp
语法:int strcmp(str1, str2);
作用:比较两个字符串的ASCII码,若str1<\str2,则返回一个小于零的数字,若str1==str2,则返回一个等于零的数字,若str1>str2,则返回一个大于零的数字
8.strtok
语法:char *strtok(str, const char *sep);
sep是个字符串,定义了用分隔符的字符集合
作用:根据分隔符切割一次str字符串,返回一个子字符串的指针,如果str是NULL,则从上一次调用后的位置继续分割字符串
注意:strtok会直接修改传入的字符串,所以str一般是字符串的拷贝
使用例:
#include <stdio.h>
#include <string.h>
int main() {
char str[] = "Hello world, how are you?";
char *p = " ";
for (char *ret = strtok(arr, p); ret != NUll; ret = strtok(NULL, p)) {
printf("%s\n", token);
}
return 0;
}
输出结果为
Hello
world,
how
are
you?
9.strerror
语法:char *strerror(错误码);
作用:根据错误码,打印错误信息,错误码一般是变量errno
errno是头文件<errno.h>中的一个全局的错误码的变量,当C语言的库函数在执行时,发生了错误,就会把对应的错误码赋值到errno中
10.字符判断函数
11.字符转换函数
函数:tolower()和toupper
作用:转换大小写,支持ASCII码转换
十二.内存函数
1.memcpy
语法:void *memcpy(void *destination, const void *source, int size);
作用:将一个任意类型的数组source拷贝到destination中,最多size个元素
注意事项:
- destination和source不能有内存重叠
- destination和source类型要相同
- destination的长度要大于source的长度
2.memmove
语法:void *memcpy(void *destination, const void *source, int num);
作用:将一个任意类型的数组source拷贝到destination中,最多size个元素
与memcpy的区别:在C语言标准中,memcpy只能处理不重叠的内存(某些编译器中可以处理重叠的),memmove用于处理重叠内存的拷贝
3.memcmp
语法:int memcmp(const void *p1, const void *p2, num);
作用:比较内存大小,若p1<\p2,则返回一个小于零的数字,若p1==p2,则返回一个等于零的数字,若p1>p2,则返回一个大于零的数字,一共比较size个字节
4.memset
语法:void *memset(void *destination, int c, size_t count);
c是要设置的字符,count是一个无符号数字,是要设置为指定值的字节数
作用:内存设置,将指定的内存区域设置为指定的值,常用于初始化数组或结构体等数据结构
注意点:
- memset函数是按字节进行设置的,因此在设置非字符类型(比如整型)的数组时,可能会导致数据不符合预期,特别是在涉及到字节序的情况下
- 在使用memset函数时,要确保不会越界访问内存,否则会导致未定义的行为
- memset函数的性能通常很高,因为它可以利用处理器的特殊指令进行优化
十三.结构体
1.基本使用
声明语法:
struct stu{
成员列表;
}结构体变量列表; //最后的分号不能丢
创建一个结构体变量:struct stu s1;
初始化方法:struct stu s1 = {成员变量的值};
声明的特殊写法
struct{
memberList;
}x;
匿名结构体类型,x是唯一的结构体变量
struct{
}* p;
这样写,p就是结构体指针
结构体的自引用
struct student{
struct student n;
};
这种自引用的写法的内存无法判断,运行就会报错
正确的写法:
struct student{
struct student *p;
};
这种写法的结构体内存是确定的,所以可以这么写
写法2:
typedef struct student{ //不能写成匿名结构体
struct student *p;
}student;
2.结构体内存对齐
结构体的对齐规则:
- 第一个成员在与结构体变量偏移量为0的地址处
- 其它成员变量要对齐到对齐数的整数倍的地址处
- 对齐数=编译器默认的一个对齐数与该成员变量大小中的较小值,VS编译器的默认对齐数是8,编译器可能没有默认对齐数
- 结构体的总大小为最大对齐数的整数倍
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
内存对齐存在的理由:
- 不是所有平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常
- 数据结构(尤其是栈)应该尽可能在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对其的内存访问仅需一次
总的来说:内存对齐是拿空间换时间的做法
修改默认对齐数:
#pragma pack(对齐数) //设置默认对其数
代码
#pragma pack() //取消设置的默认对齐数(可以不写,但是对齐数就永远都会是设置的对齐数)
计算偏移量:size_t offsetof(结构体名, 成员变量名);
3.结构体传参
将结构体传给函数:
void Init(struct stu *s1){
s1->name = "a";
s1->sex = 1;
}
int main(){
struct stu s1 = {0};
Init(&s1); //最好传地址
return 0;
}
4.位段
位段的声明和结构体是类似的,有两个不同:
- 位段的成员必须是int、unsigned int、signed int
- 位段的成员名后边有一个冒号和一个数字
位段的内存分配:
- 位段的成员可以是int、unsigned int、signed int和char(属于整型家族)
- 位段的空间是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的
- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段
位段的跨平台问题:
总结:跟结构体比,位段可以达到同样的效果,但是更省空间,但是有跨平台的问题
位段的应用:
5.枚举
枚举类型的声明:
enum sex{
man,
woman,
helicopter
};
创建一个枚举变量:enum sex s = man;
枚举的优点:
- 可读性和可维护性强
- 防止命名污染
- 和#define定义的标识符比较枚举有类型检查,更加严谨
- 便于调试
- 使用方便,一次可以定义多个常量
6.联合
联合也是一种特殊的自定义类型,这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)
当我们修改其中一个成员时,其他成员的值也会发生变化,因为它们共享同一块内存空间
联合的内存计算:联合的大小至少是最大成员的内存,最终会是对齐数的整数倍
联合的声明:
union S{
...
};
判断大小端存储模式:
int check_sys(){
union{
char c;
int i;
}u;
u.i = 1;
return u.c; //1是小端,0是大端
}
十四.内存动态分配
1.malloc
语法:void malloc(size_t);
size_t是要申请获取的字节大小
注意:malloc的返回值是void,使用时最好强制转换一下
使用:
#include <errno.h>
#include <string.h>
#include <stdlib.h> //malloc和free都是声明在<stdlib.h>中的
#include <stdio.h>
int main(){
int *p = (int*)malloc(20); //申请5个整型的空间
if (p == NULL){
printf("%s\n", strerror(errno)); //如果请求失败,则返回空指针
}else{
for (int i = 0; i < 5; i++){
*(p + i) = i;
}
}
return 0;
}
2.free
语法:void free(void* p)
作用:释放空间,将分配给指针p的空间返回栈区,p指针不会重置为NULL
注意:
- malloc在程序结束时也会自动释放空间,free是主动释放、
- 如果参数p指向的空间不是动态开辟的,那free函数的行为是未定义的
- 若果参数p是NULL指针,则函数什么都不做
3.calloc
作用:开辟一个数组空间,并初始化为0
语法:void *calloc(size_t num, size_t size);
num是元素个数,size是一个元素的大小
如果开辟失败,则返回NULL指针
和malloc的区别:初始化为0
4.realloc
作用:调整动态开辟的空间的大小
语法:void *realloc(void *空间指针, size_t newSize);
newSize是新的空间大小
注意事项:
- 如果p指向的空间之后有足够的内存空间可以追加,则直接追加,后返回p
- 如果p指向的空间之后没有足够的内存空间可以追加,则返回一个新的地址
- 建议用一个新的变量来接受realloc函数的返回值
常见的动态内存错误
①对空指针解引用
②对动态开辟内存的越界访问
③对非动态开辟的空间使用free
④使用free释放动态内存的一部分
⑤对同一块动态内存的多次释放
⑥动态开辟内存忘记释放(内存泄露)
经典的笔试题
①:
输出结果是程序崩溃,原因是str是将自己的值而不是自己的地址传给GetMemory函数,也就是说p是str的一份拷贝,给p赋值不会传递给str,str始终是空指针
其次这段代码中有内存泄露的问题
返回栈空间的地址的问题:str指向p但是p在执行完函数后就会被销毁,于是str指向未知地址
所以不能返回栈空间地址,可以用static修饰,但是堆空间不会被及时销毁,所以也可用malloc分配空间
5.柔性数组
C99规定:结构中的最后一个元素允许是未知大小的数组,这就叫柔性数组成员
例:
struct S{
int n;
int arr[0]; //未知大小的柔性数组成员
};
int main(){
struct S* ps = (struct S*)malloc(sizeof(struct S)+5*sizeof(int)); //第一个sizeof不会计算柔性数组的大小,结果是4
if (ps != NULL) { // 检查内存是否成功分配
ps->arr[0] = 0; // 将arr的第一个元素赋值为0
// 在程序结束前释放动态分配的内存
free(ps);
}
return 0;
}
连续内存访问效率高,比如上述代码中的n和arr
柔性数组的特点:
- 结构中的柔性数组成员前面必须至少有一个其它成员
- sizeof返回的这种结构大小不包括柔性数组的内存
- 包含柔性数组成员的结构用malloc函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小
优点:方便内存释放,访问速度高
十五.文件操作
1.文件
文件分为程序文件和数据文件
数据文件根据数据的组织形式,数据文件被称为文本文件(ASCII码)或二进制文件
文件缓冲区:ANSIC标准采用缓冲文件系统处理数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块文件缓冲区
缓冲区分输出缓冲区和输入缓冲区,缓冲区的大小由C编译系统决定
文件指针(文件类型指针):
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息。这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的,为FILE
一般都是通过一个FILE的指针来维护这个FILE结构的变量:FILE *pf;
2.文件的打开和关闭
ANSIC规定使用fopen函数来打开文件,fclose关闭文件(都是stdio中的函数)
语法:
FILE *fopen(const char *文件名, const char *打开方式);
int fclose(FILE *stream);
打开方式:
文件的顺序读写:
fgetc的语法:int fgetc(FILE *stream);(如果stream是stdin,则表示从键盘读取文本行,fgets同理)
fputc的语法:int fputc(int character, FILE *stream);(character参数是要写入的字符,以ASCII码形式表示)
fgets的语法:char *fgets(char *str, int n, FILE *stream);(n表示要读取的最大字符数,返回的是指向str的指针)
fputs的语法:int fputs(const char *str, FILE *stream);
fscanf的语法:int fscanf(FILE *stream, const char *format, …);(根据指定的格式字符串从文件中读取数据,format是"%s"之类的)
fprintf的语法:int fprintf(FILE *stream, const char *format, …);(根据指定的格式字符串将数据写入到文件中)
fread的语法:size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);(ptr是存储数据的指针,size是每个数据项的大小,nmemb是要读取或写入的数据项的数量,返回的指针指向ptr)
fwrite的语法:size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
几个scanf和printf的对比
scanf:
用于从标准输入(键盘)读取输入。
格式化输入函数,可以根据格式字符串从标准输入中读取数据。
例子:scanf(“%d”, &num);
printf:
用于向标准输出(屏幕)输出内容。
格式化输出函数,可以按照格式字符串将数据输出到屏幕。
例子:printf(“The value of num is %d”, num);
fscanf:
用于从文件中读取输入。
格式化输入函数,可以根据格式字符串从文件中读取数据。
例子:fscanf(file_ptr, “%d”, &num);
fprintf:
用于向文件中写入内容。
格式化输出函数,可以按照格式字符串将数据输出到文件中。
例子:fprintf(file_ptr, “The value of num is %d”, num);
sscanf:
用于从字符串中读取输入。
格式化输入函数,可以根据格式字符串从字符串中读取数据。
例子:sscanf(str, “%d”, &num);
sprintf:
用于将格式化的数据写入字符串中。
格式化输出函数,可以按照格式字符串将数据输出到字符串中。
例子:sprintf(str, “The value of num is %d”, num);
总的来说,scanf、printf用于标准输入输出,fscanf、fprintf用于文件输入输出,sscanf、sprintf用于字符串处理
3.其它函数
(1)fseek
语法:int fseek(FILE *stream, long offset, int origin);(stdio.h中的)
offset是偏移量,origin是文件指针的当前位置
作用:将文件指针从origin这个位置偏移offset个字节
一些常量:
- SEEK_CUR:文件指针的当前位置
- SEEK_END:文件的末尾位置
- SEEK_SET:文件起始位置
(2)ftell
语法:long int ftell(FILE *stream);
作用:获取stream的相对于文件开头的偏移量
(3)fwind
语法:void rewind(FILE *stream);
作用:将stream重新指向文件开头
(4)文件结束判定
feof函数的语法:int feof(FILE *stream);
feof函数的作用:在文件读取结束的时候,判断是读取失败还是遇到文件结尾结束,如果失败则返回0,如果遇到文件结尾,则返回非负值
函数perror(str);可以打印str+报错信息且是stdio中的函数,比sterror(errno)方便
十六.程序环境和预处理
1.翻译环境和运行环境
在ANSI C的任何一种实现中,存在两个不同的环境
第一个是翻译环境,在这个环境中源代码被转换为可执行的机器指令
第二个执行环境,它用于实际执行代码
每个源文件都会被编译器处理,编译成目标文件(add.c->add.obj)
然后目标文件通过链接器链接成可执行文件(add.obj->add.exe)
编译又分为三个阶段:
- 预编译(文本操作):将include引入的头文件展开成代码,并把注释删除,使用空格代替注释,替换#define的文本
- 编译:把c语言代码翻译成汇编代码(语法分析、词法分析、语义分析、符号汇总)
- 汇编