C语言提高
1.数据类型
每个数据必须指定类型,由编译器创建的,为了更好的管理内存
- 基本类型
- 整型
- 字符型
- 浮点型
- 单精度
- 双精度
- 构造类型
- 数组类型
- 结构体
- 联合体
- 枚举
- 指针类型
2. typedef使用
-
起别名 - 简化struct关键字
-
区分数据类型
-
提高代码移植性
3. void使用
不可以利用void创建变量 无法给无类型变量分配内存
用途:
-
限定函数返回值,函数参数
-
void * 万能指针 可以不通过强制类型转换就转成其他类型指针
4. sizeof用法
本质:不是一个函数,是一个操作符
返回值类型 unsigned int无符号整型
用途:可以统计数组长度
数组当参数传递后使用当指针使用,存放第一个数组元素地址
5. 变量的修改方式
-
直接修改
-
间接修改:指针
6. 内存分区
-
运行前
- 代码区:只读,共享
- 数据区:存放数据:全局变量 、静态变量、常量
- 已初始化数据区 data:已初始化全局变量、静态变量(全局和局部)、常量数据。
- 未初始化数据区 bss :未初始化的全局变量和静态变量。
-
运行后、
-
栈 符合先进后出数据结构,编译器自动管理分配和释放,有限容量
不要返回局部变量的地址,局部变量在函数执行之后就被释放了,释放的内存就没有权限取操作了,如果操作结果未知
-
堆 容量远远大于栈,不是无限。手动开辟 malloc 手动释放 free
主调函数没有分配内存,被调函数需要用更高级的指针去修饰低级指针,进行分配内存
-
7. static和extern区别
- static静态变量:在程序运行前分配内存,程序运行结束生命周期结束,在本文件内都可以使用静态变量,作用域有限制
- extern外部变量:表明变量在其他文件中,编译器在全局变量会隐式默认加
8. 常量
- const修饰的变量:分为全局和局部变量,
- 全局变量。直接修改编译器报错,间接修改语法通过,运行失败。原因:收到常量区保护。
- 局部变量。直接修改报错。间接修改成功,称为伪常量。不可以用来初始化数组。
- 字符串常量:
char *p="helloworld"
,内容相同,地址相同。由编译器决定。不可以修改字符串常量。ANSI并没有制定出字符串是否可以修改的标准,根据编译器不同,可能最终结果也是不同的
9. 函数调用流程
- 宏函数
- 宏函数需要加小括号修饰,保证运算完整性
- 通常将频繁使用,短小的函数写成宏函数
- 一定程度上比普通函数效率高,省去普通函数入栈、出栈的时间(以空间换时间)
局部变量、函数形参、函数返回地址. 入栈 和 出栈
调用惯例: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("%d\n", &a);
printf("%d\n", &b);
printf("%d\n", &c);
printf("%d\n", &d);
}
//2、内存存放方向
void test02()
{
int a = 0x11223344;
char * p = &a;
printf("%x\n", *p); //44 低位字节数据 低地址
printf("%x\n", *(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 指针变量未初始化
/*int * p;
printf("%d\n",*p);*/
//2.2 指针释放后未置空
char * str = malloc(100);
free(str);
//记住释放后 置空,防止野指针出现
//str = NULL;
//free(str);
//2.3 空指针可以重复释放、野指针不可以重复释放
//2.4 指针操作超越变量作用域
int * p = doWork();
printf("%d\n", *p);
printf("%d\n", *p);
}
12. 指针的步长
-
+1之后跳跃的字节数
-
解引用 解出的字节数
-
自定义结构体做步长练习
通过 offsetof( 结构体名称, 属性) 找到属性对应的偏移量
offsetof 引入头文件 #include<stddef.h>
//1、指针的步长代表 指针+1之后跳跃的字节数
void test01()
{
char * p = NULL;
printf("%d\n", p);
printf("%d\n", p+1);
double * p2 = NULL;
printf("%d\n", p2);
printf("%d\n", p2 + 1);
}
//2、解引用的时候,解出的字节数量
void test02()
{
char buf[1024] = { 0 };
int a = 1000;
memcpy(buf + 1, &a, sizeof(int));
char * p = buf;
printf("%d\n", *(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属性的偏移量: %d\n", offsetof(struct Person, d));
printf("d属性的值为:%d\n", *(int *)((char *)&p + offsetof(struct Person, d)));
}
13. 指针的间接赋值
-
三大条件
-
一个普通变量+指针变量( 实参+形参)
-
建立关系
-
通过* 操作内存
-
利用Qt实现 操作地址 修改内存
14. 指针做函数参数的输入输出特性
-
输入特性
在主调函数中分配内存,被调函数使用
分配在栈上和堆区
-
输出特性
在被调函数中分配内存,主调函数使用
15.字符串强化训练
-
字符串结束标志 \0
-
sizeof 和 strlen
-
拷贝字符串 利用三种方式
利用[]
利用指针
while (*dest++ = *src++){}
-
翻转字符串
利用[ ]
利用指针
-
sprintf使用
格式化字符串
sprintf(目标字符串,格式化内容,占位参数…)
返回值 有效字符串长度
16. calloc 和 realloc
- calloc 和malloc 都是在堆区分配内存
-
与malloc不同的是,calloc会将空间初始化为0
-
calloc(个数,大小)
colloc(10,sizeof(int))
=malloc(sizeof(int)*10)
,成功返回空间起始地址,失败发挥null
- realloc 重新分配内存
- 如果重新分配的内存比原来大,那么不会初始化新空间为0
- 先看后续空间,如果足够,那么直接扩展
- 如果后续空闲空间不足,那么申请足够大的空间,将原有数据拷贝到新空间下,释放掉原有空间,将新空间的首地址返回
- 如果重新分配的内存比原来小,那么释放后序空间,只有权限操作申请空间
17. scanf()使用
%*s或%*d | 跳过数据 |
---|---|
%[width]s | 读指定宽度的数据 |
%[a-z] | 匹配a到z中任意字符(尽可能多的匹配) |
%[aBc] | 匹配a、B、c中一员,贪婪性 |
%[^a] | 匹配非a的任意字符,贪婪性 |
%[^a-z] | 表示读取除a-z以外的所有字符 |
18.查找子串
实现mystrstr 自己查找子串功能mystrstr
int myStrstr(char *str, char * substr)
{
int n = 0;
while (*str != '\0')
{
if (*str != *substr)
{
n++;
str++;
substr++;
}
char *tmpStr = str;
char *tmpSubStr = substr;
while (*tmpSubStr != '\0')
{
if (*tmpStr != *tmpSubStr)
{
n++;
str++;
break;
}
tmpStr++;
tmpSubStr++;
}
if (*tmpSubStr == '\0')
{
return n;
}
}
return -1;
}
void test01()
{
char * str = "abdnfcdefgdfasdfaf";
int ret = myStrstr(str, "dnf");
if (ret != -1)
{
printf("找到了子串,位置为:%d\n", ret);
}
else
{
printf("未找到子串\n");
}
}
19.指针的易错点
- 越界
- 指针叠加会不断改变指针指向
- 返回局部变量地址
- 同一块内存释放多次(不可以释放野指针)
20.二级指针做函数参数的输入输出特性
-
二级指针做函数参数的输入特性
- 创建在堆区
- 创建在栈区
-
二级指针做函数参数的输出特性
- 被调函数分配内存,主调函数使用
21. const 修饰形参,防止误操作
22. 二级指针文件操作练习
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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("%d\n", 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);
}
23. 位运算
按位取反 ~ 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) 头文件<stddef.h>
- sizeof 统计数组长度
数组名是指针常量,指针的指向不可以修改的,而指针指向的值可以改
传参数时候,int arr[] 可读性更高
int arr[] 等价于 int * arr
数组索引下标可以为负数
int * const a
指针常量 :a
是常量,指针的常量,它是不可改变地址的指针,但是可以对它所指向的内容进行修改。
const int * a
常量指针 :*a
是常量,指向常量的指针,指针所指向的地址的内容是不可修改的。
26. 数组指针的定义方式
-
先定义出数组类型,再通过类型定义数组指针变量
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 等于数组名
27. 二维数组名
- 二维数组名 除了两种特殊情况外,是指向第一个一维数组的 数组指针
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)
可读性比较高
-
数组指针 和 指针数组?
- 数组指针: 指向数组的指针
- 指针数组: 由指针组成数组
28. 指针数组排序
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 ,可以直接创建一个结构体变量
-
结构体声明 可以是匿名
-
在栈上创建和在堆区创建结构体
-
在栈上和堆区创建结构体变量数组
-
结构体深浅拷贝
- 系统提供的赋值操作是 浅拷贝 – 简单值拷贝,逐字节拷贝
- 如果结构体中有属性 创建在堆区,就会出现问题,在释放期间,一段内存重复释放,一段内存泄露
- 解决方案:自己手动去做赋值操作,提供深拷贝
30. 结构体嵌套一级指针练习
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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;
}
31. 内存对齐
查看对齐模数 #pragma pack(*show*)
默认对齐模数 8
-
自定义数据类型 对齐规则
第一个属性开始 从0开始偏移
第二个属性开始 要放在 该类型的大小 与 对齐模数比 取小的值 的整数倍
所有属性都计算完后,再整体做二次偏移,将整体计算的结果 要放在 结构体最大类型 与对齐模数比 取小的值的 整数倍上
-
结构体嵌套结构体
结构体嵌套结构体时候,子结构体放在该结构体中最大类型 和对齐模数比 的整数倍上即可
32. 文件读写回顾
-
按照字符读写
写 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
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
//按照字符读写文件: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("%s\n", 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 年龄: %d\n", temp.name, temp.age);
fclose(f_read);
}
int main(){
//test01();
//test02();
//test03();
//test04();
test05();
system("pause");
return EXIT_SUCCESS;
}
33. 函数指针
-
函数指针
函数名本质就是一个函数指针
可以利用函数指针 调用函数
-
函数指针定义方式
1、先定义出函数类型,再通过类型定义函数指针
typedef void(FUNC_TYPE)(int, char);
2、定义出函数指针类型,通过类型定义函数指针变量
typedef void( * FUNC_TYPE2)(int, char);
3、直接定义函数指针变量
void(*pFunc3)(int, char) = func;
- 函数指针和指针函数
函数指针 指向了函数的指针
指针函数 函数返回值是指针的函数
-
函数指针数组
void(*pArray[3])();
-
函数指针做函数参数(回调函数)
利用回调函数实现打印任意类型数据
提供能够打印任意类型数组函数
利用回调函数 提供查找功能
34.链表反转练习
35.预处理指令
-
头文件 #include
<> “”区别
<> 包含系统头
“” 包含自定义头
-
宏
-
宏常量 大写 可以是常数
不重视作用域
没有数据类型
不做语法检查
有效范围在文件内
利用 #undef 卸载宏
-
宏函数
将频繁、短小函数写成宏函数
优点:以空间换时间
-
-
条件编译
#ifdef #else #endif
测试存在#ifndef #else #endif
测试不存在#if #else #endif
自定义条件编译 -
特殊宏
__FILE__
宏所在文件路径__LINE__
宏所在行__DATE__
宏编译日期__TIME__
宏编译时间
36.生成库配置
-
静态库配置
右键项目->属性 ->常规->配置类型 ->静态库
生成项目 生成.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
37.递归函数
本质:函数自身调用自身
注意事项:递归函数必须有结束条件,函数有出口
void reversePrint(char * p)
{
if (*p == '\0')
{
return;
}
reversePrint(p + 1);
printf("%c\n", *p);
}