C语言提高 1.数据类型每个数据必须指定类型,由编译器创建的,为了更好的管理内存
- 基本类型
- 整型
- 字符型
- 浮点型
- 单精度
- 双精度
- 构造类型
- 数组类型
- 结构体
- 联合体
- 枚举
- 指针类型
-
起别名 - 简化struct关键字
-
区分数据类型
-
提高代码移植性
不可以利用void创建变量 无法给无类型变量分配内存
用途:
-
限定函数返回值,函数参数
-
void * 万能指针 可以不通过强制类型转换就转成其他类型指针
本质:不是一个函数,是一个操作符
返回值类型 unsigned int无符号整型
用途:可以统计数组长度
数组当参数传递后使用当指针使用,存放第一个数组元素地址
5. 变量的修改方式-
直接修改
-
间接修改:指针
-
运行前
- 代码区:只读,共享
- 数据区:存放数据:全局变量 、静态变量、常量
- 已初始化数据区 data:已初始化全局变量、静态变量(全局和局部)、常量数据。
- 未初始化数据区 bss :未初始化的全局变量和静态变量。
-
运行后、
-
栈 符合先进后出数据结构,编译器自动管理分配和释放,有限容量
不要返回局部变量的地址,局部变量在函数执行之后就被释放了,释放的内存就没有权限取操作了,如果操作结果未知
-
堆 容量远远大于栈,不是无限。手动开辟 malloc 手动释放 free
主调函数没有分配内存,被调函数需要用更高级的指针去修饰低级指针,进行分配内存
-
- static静态变量:在程序运行前分配内存,程序运行结束生命周期结束,在本文件内都可以使用静态变量,作用域有限制
- extern外部变量:表明变量在其他文件中,编译器在全局变量会隐式默认加
- const修饰的变量:分为全局和局部变量,
- 全局变量。直接修改编译器报错,间接修改语法通过,运行失败。原因:收到常量区保护。
- 局部变量。直接修改报错。间接修改成功,称为伪常量。不可以用来初始化数组。
- 字符串常量:char *p="helloworld",内容相同,地址相同。由编译器决定。不可以修改字符串常量。ANSI并没有制定出字符串是否可以修改的标准,根据编译器不同,可能最终结果也是不同的
- 宏函数
- 宏函数需要加小括号修饰,保证运算完整性
- 通常将频繁使用,短小的函数写成宏函数
- 一定程度上比普通函数效率高,省去普通函数入栈、出栈的时间(以空间换时间)
局部变量、函数形参、函数返回地址. 入栈 和 出栈
调用惯例:vs默认cdecl
主调函数和被调函数必须要有一致约定,才能正确的调用函数,这个约定我们称为调用惯例
调用惯例 包含内容: 出栈方、参数传递顺序、函数名称修饰
C/C++下默认调用惯例: cdecl 从右到左 ,主调函数管理出栈 变量名前加下划线修饰
//变量传递分析
char * func()
{
char * p = malloc(10); //堆区数据,只要没有释放,都可以使用
int c = 10;//在func中可以使用,test01和main都不可以使用
return p;
}
void test01()
{
int b = 10; // 在test01 、func 可以使用,在main中不可以用
func();
}
int main(){
int a = 10; //在main 、test01 、 func中都可以使用
test01();
system("pause");
return EXIT_SUCCESS;
}
10. 栈的生长方向和内存存放方向
栈生长方向
栈底 — 高地址
栈顶 — 低地址
内存存放方向
高位字节数据 — 高地址
低位字节数据 — 低地址
小端对齐方式
void test01()
{
int a = 10; //栈底 高地址
int b = 10;
int c = 10;
int d = 10; //栈顶 低地址
printf("%dn", &a);
printf("%dn", &b);
printf("%dn", &c);
printf("%dn", &d);
}
//2、内存存放方向
void test02()
{
int a = 0x11223344;
char * p = &a;
printf("%xn", *p); //44 低位字节数据 低地址
printf("%xn", *(p+1)); //33 高位字节数据 高地址
}
int main(){
//test01();
test02();
system("pause");
return EXIT_SUCCESS;
}
11. 空指针和野指针
-
空指针
不能向NULL或者非法内存拷贝数据
-
野指针
指针变量未初始化
指针释放后未置空
指针操作超越变量作用域
-
空指针可以重复释放、野指针不可以重复释放
//1、不能向NULL或者非法内存拷贝数据
void test01()
{
//char *p = NULL;
给p指向的内存区域拷贝内容
//strcpy(p, "1111"); //err
//char *q = 0x1122;
给q指向的内存区域拷贝内容
//strcpy(q, "2222"); //err
}
//指针操作超越变量作用域
int * doWork()
{
int a = 10;
int * p = &a;
return p;
}
//2、野指针出现情况
void test02()
{
//2.1 指针变量未初始化
//2.2 指针释放后未置空
char * str = malloc(100);
free(str);
//记住释放后 置空,防止野指针出现
//str = NULL;
//free(str);
//2.3 空指针可以重复释放、野指针不可以重复释放
//2.4 指针操作超越变量作用域
int * p = doWork();
printf("%dn", *p);
printf("%dn", *p);
}
12. 指针的步长
-
+1之后跳跃的字节数
-
解引用 解出的字节数
-
自定义结构体做步长练习
通过 offsetof( 结构体名称, 属性) 找到属性对应的偏移量
offsetof 引入头文件 #include
//1、指针的步长代表 指针+1之后跳跃的字节数
void test01()
{
char * p = NULL;
printf("%dn", p);
printf("%dn", p+1);
double * p2 = NULL;
printf("%dn", p2);
printf("%dn", p2 + 1);
}
//2、解引用的时候,解出的字节数量
void test02()
{
char buf[1024] = { 0 };
int a = 1000;
memcpy(buf + 1, &a, sizeof(int));
char * p = buf;
printf("%dn", *(int *)(p+1));
}
//步长练习,自定义数据类型练习
struct Person
{
char a; // 0 ~ 3
int b; // 4 ~ 7
char buf[64]; // 8 ~ 71
int d; // 72 ~ 75
};
void test03()
{
struct Person p = { 'a', 10, "hello world", 20 };
printf("d属性的偏移量: %dn", offsetof(struct Person, d));
printf("d属性的值为:%dn", *(int *)((char *)&p + offsetof(struct Person, d)));
}
13. 指针的间接赋值
-
三大条件
-
一个普通变量+指针变量( 实参+形参)
-
建立关系
-
通过* 操作内存
-
利用Qt实现 操作地址 修改内存
14. 指针做函数参数的输入输出特性-
输入特性
在主调函数中分配内存,被调函数使用
分配在栈上和堆区
-
输出特性
在被调函数中分配内存,主调函数使用
-
字符串结束标志
-
sizeof 和 strlen
-
拷贝字符串 利用三种方式
利用[]
利用指针
while (*dest++ = *src++){}
-
翻转字符串
利用[ ]
利用指针
-
sprintf使用
格式化字符串
sprintf(目标字符串,格式化内容,占位参数…)
返回值 有效字符串长度
- calloc 和malloc 都是在堆区分配内存
-
与malloc不同的是,calloc会将空间初始化为0
-
calloc(个数,大小)colloc(10,sizeof(int))=malloc(sizeof(int)*10),成功返回空间起始地址,失败发挥null
- realloc 重新分配内存
- 如果重新分配的内存比原来大,那么不会初始化新空间为0
- 先看后续空间,如果足够,那么直接扩展
- 如果后续空闲空间不足,那么申请足够大的空间,将原有数据拷贝到新空间下,释放掉原有空间,将新空间的首地址返回
- 如果重新分配的内存比原来小,那么释放后序空间,只有权限操作申请空间
| %*s或%*d | 跳过数据 |
|---|---|
| %[width]s | 读指定宽度的数据 |
| %[a-z] | 匹配a到z中任意字符(尽可能多的匹配) |
| %[aBc] | 匹配a、B、c中一员,贪婪性 |
| %[^a] | 匹配非a的任意字符,贪婪性 |
| %[^a-z] | 表示读取除a-z以外的所有字符 |
实现mystrstr 自己查找子串功能mystrstr
int myStrstr(char *str, char * substr)
{
int n = 0;
while (*str != ' ')
{
if (*str != *substr)
{
n++;
str++;
substr++;
}
char *tmpStr = str;
char *tmpSubStr = substr;
while (*tmpSubStr != ' ')
{
if (*tmpStr != *tmpSubStr)
{
n++;
str++;
break;
}
tmpStr++;
tmpSubStr++;
}
if (*tmpSubStr == ' ')
{
return n;
}
}
return -1;
}
void test01()
{
char * str = "abdnfcdefgdfasdfaf";
int ret = myStrstr(str, "dnf");
if (ret != -1)
{
printf("找到了子串,位置为:%dn", ret);
}
else
{
printf("未找到子串n");
}
}
19.指针的易错点
- 越界
- 指针叠加会不断改变指针指向
- 返回局部变量地址
- 同一块内存释放多次(不可以释放野指针)
-
二级指针做函数参数的输入特性
- 创建在堆区
- 创建在栈区
-
二级指针做函数参数的输出特性
- 被调函数分配内存,主调函数使用
#define _CRT_SECURE_NO_WARNINGS #include23. 位运算#include #include int getGetFileLines(FILE *fp) { if (fp == NULL) { return -1; } char buf[1024] = { 0 }; int lines = 0; while (fgets(buf,1024,fp)!=NULL) { lines++; } fseek(fp, 0, SEEK_SET); return lines; } void readFileData(FILE *fp, int len, char **pArray) { if (fp == NULL) { return; } if (len <= 0) { return; } if (pArray ==NULL) { return; } char buf[1024] = { 0 }; int i = 0; while (fgets(buf, 1024, fp) != NULL) { int curLen = strlen(buf) + 1; char * curStr = malloc(curLen); strcpy(curStr, buf); pArray[i++] = curStr; memset(buf, 0, 1024); } } void showFileData(char ** pArray, int len) { for (int i = 0; i < len; i++) { printf("%s", pArray[i]); } } void test01() { FILE *fp = fopen("./test.txt", "r+"); if (fp == NULL) { printf("文件打开失败"); return; } int len = getGetFileLines(fp); printf("%dn", len); char **pArray = malloc(sizeof(char *)* 5); readFileData(fp, len, pArray); showFileData(pArray, len); //释放堆区内容 for (int i = 0; i < len; i++) { if (pArray[i] != NULL) { free(pArray[i]); pArray[i] = NULL; } } free(pArray); pArray = NULL; //关闭文件 fclose(fp); }
按位取反 ~ 0变1 1 变0
按位与 & 全1为1 一0为0
按位或 | 全0为0 一1为1
按位异或 ^ 相同为0 不同为1
24. 位移运算左移运算 << X 乘以2 ^ X
右移运算 >> X 除以 2 ^X
有些机器用0填充高位
有些机器用1填充高位
如果是无符号,都是用0填充
25. 一维数组名-
除了两种特殊情况外,都是指向数组第一个元素的指针
- sizeof 统计数组长度int arr[5]={1,2,3,4,5} sizeof(arr)为20
- 对数组名取地址, &arr数组指针,步长整个数组长度
指针步长:offset(struct Person,d) 头文件
26. 数组指针的定义方式数组名是指针常量,指针的指向不可以修改的,而指针指向的值可以改
传参数时候,int arr[] 可读性更高int arr[] 等价于 int * arr
数组索引下标可以为负数
int * const a指针常量 :a是常量,指针的常量,它是不可改变地址的指针,但是可以对它所指向的内容进行修改。
const int * a 常量指针 :*a是常量,指向常量的指针,指针所指向的地址的内容是不可修改的。
-
先定义出数组类型,再通过类型定义数组指针变量
int arr[5] = {1,2,3,4,5}; typedef int(ARRARY_TYPE)[5];//ARRARY_TYPE ARRARY_TYPE * arrP = &arr; //*arrP = arr = 数组名代表存放5个int类型元素的数组 的数组类型
-
先定义数组指针类型,再通过类型定义数组指针变量
typedef int(*ARRARY_TYPE)[5]; ARRARY_TYPE arrP = &arr;
-
直接定义数组指针变量
int(* p )[5] = &arr; // *p 等于数组名
- 二维数组名 除了两种特殊情况外,是指向第一个一维数组的 数组指针
- sizeof 统计二维数组大小
- 对数组名称取地址 int(*p)[3][3] = &arr
-
二维数组做函数参数
- void printArray(int (*array)[3], int row, int col)
- void printArray(int array[][3], int row ,int col)
- void printArray(int array[3][3], int row ,int col) 可读性比较高
-
数组指针 和 指针数组?
- 数组指针: 指向数组的指针
- 指针数组: 由指针组成数组
void sortArray(char **pArray,int len)
{
int min=0;
int i = 0, j =0;
for (i = 0; i < len; i++)
{
min = i;
for (j = i + 1; j < len; j++)
{
if (strcmp(pArray[j],pArray[min]) == -1)
{
min = j;
}
}
if (i != min)
{
char * tmp="";
tmp = pArray[i];
pArray[i] = pArray[min];
pArray[min] = tmp;
}
}
}
29. 结构体基本概念
-
加typedef 可以给结构体起别名
-
不加typedef ,可以直接创建一个结构体变量
-
结构体声明 可以是匿名
-
在栈上创建和在堆区创建结构体
-
在栈上和堆区创建结构体变量数组
-
结构体深浅拷贝
- 系统提供的赋值操作是 浅拷贝 – 简单值拷贝,逐字节拷贝
- 如果结构体中有属性 创建在堆区,就会出现问题,在释放期间,一段内存重复释放,一段内存泄露
- 解决方案:自己手动去做赋值操作,提供深拷贝
#define _CRT_SECURE_NO_WARNINGS #include31. 内存对齐#include #include struct Person { char *name; int age; }; struct Person ** allocateSpace() { struct Person ** tmp = malloc(sizeof(struct Person *) * 3); for (int i = 0; i < 3; i++) { tmp[i] = malloc(sizeof(struct Person)); tmp[i]->name = malloc(1024); //strcpy(tmp[i]->name,"") sprintf(tmp[i]->name, "name[%d]", i + 100); tmp[i]->age = i + 100; } return tmp; } void printPerson(struct Person ** pArray, int len ) { for (int i = 0; i < len; i++) { printf("%s : %d", pArray[i]->name, pArray[i]->age); printf("n"); } } void freeSpace(struct Person ** pArray, int len) { for (int i = 0; i < len; i++) { if (pArray[i]->name != NULL) { free(pArray[i]->name); pArray[i]->name = NULL; } } for (int i = 0; i < len; i++) { if (pArray[i] != NULL) { free(pArray[i]); pArray[i] = NULL; } } if (pArray != NULL) { free(pArray); pArray = NULL; } } void test01() { struct Person ** pArray = NULL; pArray = allocateSpace(); //打印数组 printPerson(pArray, 3); //释放内存 freeSpace(pArray,3); printPerson(pArray, 3); pArray = NULL; } int main() { test01(); system("pause"); return EXIT_SUCCESS; }
查看对齐模数 #pragma pack(*show*)
默认对齐模数 8
-
自定义数据类型 对齐规则
第一个属性开始 从0开始偏移
第二个属性开始 要放在 该类型的大小 与 对齐模数比 取小的值 的整数倍
所有属性都计算完后,再整体做二次偏移,将整体计算的结果 要放在 结构体最大类型 与对齐模数比 取小的值的 整数倍上
-
结构体嵌套结构体
结构体嵌套结构体时候,子结构体放在该结构体中最大类型 和对齐模数比 的整数倍上即可
-
按照字符读写
写 fputc
读 fgetc
while ( (ch = *fgetc*(f_read)) != *EOF* ) 判断是否到文件尾
-
按行读写
写 fputs
读 fgets
-
按块读写
写 fwrite
参数1 数据地址 参数2 块大小 参数3 块个数 参数4 文件指针
读 fread
-
格式化读写
写 fprintf
读 fscanf
-
随机位置读写
fseek( 文件指针, 偏移, 起始位置 )
SEEK_SET 从头开始
SEEK_END 从尾开始
SEEK_CUR 从当前位置
rewind 将文件光标置首
error宏 利用perror打印错误提示信息
#define _CRT_SECURE_NO_WARNINGS #include33. 函数指针#include #include //按照字符读写文件:fgetc(), fputc() void test01() { //写文件 FILE * f_write = fopen("./test01.txt", "w+"); if (f_write == NULL) { return; } char buf[] = "this is first test"; for (int i = 0; i < strlen(buf);i++) { fputc(buf[i], f_write); } fclose(f_write); //读文件 FILE * f_read = fopen("./test01.txt", "r"); if (f_read == NULL) { return; } char ch; while ( (ch = fgetc(f_read)) != EOF ) // EOF End of File { printf("%c", ch); } fclose(f_read); } //按照行读写文件:fputs(), fgets() void test02() { //写文件 FILE * f_write = fopen("./test02.txt", "w"); if (f_write == NULL) { return; } char * buf[] = { "锄禾日当午n", "汗滴禾下土n", "谁知盘中餐n", "粒粒皆辛苦n", }; for (int i = 0; i < 4;i++) { fputs(buf[i], f_write); } fclose(f_write); //读文件 FILE * f_read = fopen("./test02.txt", "r"); if (f_read == NULL) { return; } while ( !feof(f_read )) { char buf[1024] = { 0 }; fgets(buf, 1024, f_read); printf("%s", buf); } fclose(f_read); } //按照块读写文件:fread(), fwirte() struct Hero { char name[64]; int age; }; void test03() { //写文件 wb二进制方式 FILE * f_write = fopen("./test03.txt", "wb"); if (f_write == NULL) { return; } struct Hero heros[4] = { { "亚瑟" , 18 }, { "赵云", 28 }, { "妲己", 19 }, { "孙悟空", 99 }, }; for (int i = 0; i < 4;i++) { //参数1 数据地址 参数2 块大小 参数3 块个数 参数4 文件指针 fwrite(&heros[i], sizeof(struct Hero), 1, f_write); } fclose(f_write); //读文件 FILE * f_read = fopen("./test03.txt", "rb"); // read binary if (f_read == NULL) { return; } struct Hero temp[4]; //参数1 数据地址 参数2 块大小 参数3 块个数 参数4 文件指针 fread(&temp, sizeof(struct Hero), 4, f_read); for (int i = 0; i < 4;i++) { printf("姓名:%s 年龄:%d n", temp[i].name, temp[i].age); } fclose(f_read); } //按照格式化读写文件:fprintf(), fscanf() void test04() { //写文件 FILE * f_write = fopen("./test04.txt", "w"); if (f_write == NULL) { return; } fprintf(f_write, "hello world %d年 %d月 %d日", 2018, 7, 5); //关闭文件 fclose(f_write); //读文件 FILE * f_read = fopen("./test04.txt", "r"); if (f_read == NULL) { return; } char buf[1024] = { 0 }; while (!feof(f_read)) { fscanf(f_read, "%s", buf); printf("%sn", buf); } fclose(f_read); } //按照随机位置读写文件 void test05() { FILE * f_write = fopen("./test05.txt", "wb"); if (f_write == NULL) { return; } struct Hero heros[4] = { { "亚瑟", 18 }, { "赵云", 28 }, { "妲己", 19 }, { "孙悟空", 99 }, }; for (int i = 0; i < 4; i++) { //参数1 数据地址 参数2 块大小 参数3 块个数 参数4 文件指针 fwrite(&heros[i], sizeof(struct Hero), 1, f_write); } fclose(f_write); //读取妲己数据 FILE * f_read = fopen("./test05.txt", "rb"); if (f_read == NULL) { //error 宏 //printf("文件打开失败n"); perror("文件打开失败"); return; } //创建临时结构体 struct Hero temp; //改变文件光标位置 fseek(f_read, sizeof(struct Hero) *2, SEEK_SET); fseek(f_read, -(long)sizeof(struct Hero) * 2, SEEK_END); rewind(f_read); //将文件光标置首 fread(&temp, sizeof(struct Hero), 1, f_read); printf("姓名: %s 年龄: %dn", temp.name, temp.age); fclose(f_read); } int main(){ //test01(); //test02(); //test03(); //test04(); test05(); system("pause"); return EXIT_SUCCESS; }
-
函数指针
函数名本质就是一个函数指针
可以利用函数指针 调用函数
-
函数指针定义方式
1、先定义出函数类型,再通过类型定义函数指针
typedef void(FUNC_TYPE)(int, char);
2、定义出函数指针类型,通过类型定义函数指针变量
typedef void( * FUNC_TYPE2)(int, char);
3、直接定义函数指针变量
void(*pFunc3)(int, char) = func;
- 函数指针和指针函数
函数指针 指向了函数的指针
指针函数 函数返回值是指针的函数
-
函数指针数组
void(*pArray[3])();
-
函数指针做函数参数(回调函数)
利用回调函数实现打印任意类型数据
提供能够打印任意类型数组函数
利用回调函数 提供查找功能
-
头文件 #include
<> “”区别
<> 包含系统头
“” 包含自定义头
-
宏
-
宏常量 大写 可以是常数
不重视作用域
没有数据类型
不做语法检查
有效范围在文件内
利用 #undef 卸载宏
-
宏函数
将频繁、短小函数写成宏函数
优点:以空间换时间
-
-
条件编译
#ifdef #else #endif 测试存在
#ifndef #else #endif 测试不存在
#if #else #endif 自定义条件编译
-
特殊宏
__FILE__ 宏所在文件路径
__LINE__ 宏所在行
__DATE__ 宏编译日期
__TIME__ 宏编译时间
-
静态库配置
右键项目->属性 ->常规->配置类型 ->静态库
生成项目 生成.lib文件
将.lib和 .h交给用户
测试
静态库链接在编译期完成,在链接阶段复制到程序中,程序运行时与函数库再无瓜葛,移植方便,浪费空间
-
动态库配置
右键项目->属性 ->常规->配置类型 ->动态库 .dll
生成项目 生成 .lib .dll
静态库中生成的.lib和动态库生成的.lib是不同的,动态库中的.lib只会放变量的声明和 导出函数的声明,函数实现体放在.dll中
库中声明 导出函数/外部函数 : __declspec(dllexport)int mySub(int a, int b);
测试
在测试中使用#pragma comment( lib,“./mydll.lib”)
或者添加已有文件.dll .lib .h
本质:函数自身调用自身
注意事项:递归函数必须有结束条件,函数有出口
void reversePrint(char * p)
{
if (*p == ' ')
{
return;
}
reversePrint(p + 1);
printf("%cn", *p);
}



