指针:
什么是指针:指针是一种数据类型(无符号整数,代表内存编号),使用它定义指针变量。
0~4G(32个1)4294967295 byte
什么情况下使用指针:
1、函数之间共享变量(全局变量有命名冲突,不会被释放,浪费内存)
2、优化传递效率
因为C语言采用的是值传递(内存拷贝),会随着变量字节数的增加而降低运行效率。
而传递变量的地址,永远只拷贝4|8字节。
void func(const int * p);
但使用指针变量的值可能会被修改,可以配合const进行保护。
3、配合堆内存
如何使用指针:
定义:类型 *变量名_p;
1、与普通变量一样,默认值不确定,为了安全一般初始化NULL。
2、一个*只能定义一个指针变量
int *p1,*p2;
3、指针变量与普通的用法不同,为了避免混用,一般从名字上加以区别。
4、指针变量的类型决定了解决引用时访问的字节数。
赋值:变量名_p = 地址;
int* p = NULL;
1、注意地址的类型
2、void*可以与任意类型的指针进行自动转换(C++中不可以)
3、要保障地址与物理内存有对应关系(映射过)。
解引用:*p;
根据指针变量中存储的内存编号,而访问内存中的数据。
这个过程可以会有段错误,但这是由于赋值了有问题的地址。
使用指针要注意的问题:
1、野指针:指向的目标不确定,解引用时不一定会出错,但未知的危险最可怕。
而且野指针一旦产生就无法分辨,而预防的方法就是不制造野指针。
1、定义指针时一定要初始化。
2、指向的目标被释放后,要及时置空。
3、不要指向随时可能被释放的目标。
2、空指针:指针变量的值等于NULL,对这个地址解引用访问时,一定会产生段错误。
因为它存储的是操作系统重启时所需要的数据。
而预防的方法就是解引用前判断(来历不明) if(NULL == p)
指针的运算:
指针+/-整数 = 指针+/-(宽度)*整数
指针-指针 = (指针-指针)/宽度
指针与数组名:
1、数组名就一个特殊的地址,它就代表数组的第一个元素的首地址,也能当指针使用。
arr[i] <=> *(地址+i);
因此指针也能使用[]运算符
2、指针与目标内存是指向关系,而数组名是对应关系。
3、数组当函数的参数就蜕变为了指针变量,长度丢失,安全性不保障。
void fun(int* const arr,size_t len);
指针与const的配合使用:
const int* p;
int const * p;
int * const p;
const int * p;
int const * const p;
指针的高级应用:
指针数组:可以把无序的离散的数据,归纳到一起。
数组指针:专门指向数组指针
二级指针:指向指针的指针
函数指针:指向函数的指针
字符串:
由字符组成的串型数据结构,它的结束标志是'\0'。
字符串存在的形式:
字符数组:char arr[5] = {'a','b','c','d'};
一般存储在栈,也可以存储在堆。
要考虑'\0'的位置
字符串字面值:由双引号包括的若干个字符,"hehe"。
以地址形式存在,需要使用const char* str;指针指向。
数据存在只读段,如果强行修改只会出现段错误。
背后隐藏着'\0';
char str[] = "hehe";
一般使用字符串字面值来初始化字符数组。
字符串的输出:
printf %s,puts,fprintf
字符串的输入:
scanf %s:不能输入空格
gets:不限制长度
fgets:可能会接受到'\n',或者输入缓冲区中残留数据
字符串常见的操作:
strlen/strcat/strcpy/strcmp
strncat/strncpy/strncmp
memset/memcpy/strstr/strchr
sprintf/sscanf 用于拼接/解析字符串,非常好用
字符数据 -> 数据 计算 数据 -> 字符数据
堆内存管理
C语言中没有内存管理的语句,只能借助标准库中的函数进行管理堆内存。
void *malloc(size_t size);
void free(void *ptr);
void *calloc(size_t nmemb, size_t size);
void *realloc(void *ptr, size_t size);
当向malloc首次申请内存时,malloc手中也没有内存,malloc会向系统申请,系统会映射33页内存交给malloc管理,
之后再向malloc申请内存,malloc会直接从33页内存中分配,直到33页用完,再向操作系统申请。
访问malloc分配的内存时,可以越界,但不要超过33页范围。
内存泄漏:
1、指针管理失误,指向其他位置。
2、free语句没有执行到。
3、free语句忘记写。
就内存没释放,有申请新内存,导致可用的内存越来越少,速度越来越慢。
前提:程序没有结束,当程序结束后属于它的所有资源都会被系统回收。
内存碎片:
已经释放的内存,但不能被再次使用,这叫内存叫做内存碎片。
内存碎片不是错误,它是由于内存的释放时间和分配时间不协调造成的。
内存碎片无法避免(天然形成的),只能尽量减少:
1、尽量使用栈内存,只有在数据量比较多的时候再使用堆内存。
2、尽量申请大块内存自己管理。
3、不要频繁的申请释放内存。
预处理指令:
把C代码翻译成标准的C代码叫预处理、负责翻译的程序叫预处理器、被翻译的代码叫预处理指令。
查看预处理的结果:
gcc -E code.c 直接查看预处理的结果
gcc -E code.c -o code.i 把预处理的结果保存到文件中
宏定义:
宏常量:用一个有意义的单词代表一个字面值数据在代码中使用,在预处理时把单词替换成数据
优点:提高可读性、安全、扩展方便
宏函数:宏函数不是真正的函数,是带参数的宏,只是使用的方法类似函数
预处理时参数会代入到表达式中,宏名会替换成后面的表达式。
优点:运行速度快(没有参数传递),类型通用,只有极精简的代码段才适合定义宏函数
缺点:不会进行类型检查,也没有返回值,只有一个计算结果,大量使用会增加代码段的冗余。
预定义的宏:
__FILENAME__
__func__
__DATE__
__TIME__
__LINE__
条件编译:
#if
#elif
#else
#endif
#ifndef
#ifndef
头文件卫士
复合数据类型:
结构 struct
设计数据类型
typedef struct Student
{
char name[20];
char sex;
short age:1;
}Student;
定义结构变量
Student stu;
Student* stup = malloc(sizeof(Student));
访问结构成员
stu.name,stu.sex,stu.age
stup->name,stup->sex,stup->age
计算结构的字节数:
注意:成员的顺序不同会影响结构的字节数。
对齐:假定从零地址开始,每成员的起始地址编号,必须是它本身字节数的整数倍。
补齐:结构的总字节数必须是它最大成员的整数倍。
注意:在Linux系统下计算补齐、对齐时,成员超过4字节按4字节计算
联合 union
从语法上来说与结构的用法基本类似,每个成员都从零地址开始,所有成员共有一块内存。
1、使用联合判断大小端
2、联合的总字节数计算,不需要对齐,但有补齐
枚举 enum
值受限的int类型,把变量合法的值列举出来,除此以外不能等于其他的值
枚举值是常量,可以直接使用在case后,常与switch语句配合使用
文件操作:
文件分类:
文本文件:记录的是字符串的二进制
二进制文件:直接把数据补码记录到文件中
文件打开:
FILE *fopen(const char *path, const char *mode);
"r" 以只读方式打开文件,如果文件不存在则打开失败,返回值为空。
"r+" 在"r"的基础上增加写权限。
"w" 以只写方式打开文件,如果文件不存在则创建,如果文件存在则把内容清空。
"w+" 在"w"的基础上增加读取权限。
"a" 以只写方式打开文件,如果文件不存在则创建,如果文件存在则把内容保留,与"w"区别是当有新数据写入,会追加到文件的末尾。
"a+" 在"a"的基础上增加读权限。
"b" 在linux系统下没有用,表示以二进制格式打开文件。
在Windows系统下不加b '\n' 写到文件中 系统会写入'\n\r',加b则写'\n'时只写入'\n'.
读写文本内容:
int fprintf(FILE *stream, const char *format, ...);
功能:把数据以文本形式写入到文件中
stream:文件指针,fopen函数的返回值
format:格式化控制符,点位符等
...:要写入的变量。
返回值:成功写入的变量个数。
int fscanf(FILE *stream, const char *format, ...);
功能:从文件中读取数据到变量,要求文件的内容是字符。
stream:文件指针,fopen函数的返回值
format:格式化控制符,点位符等
...:变量的地址
返回值:成功读取到返回0,失败返回-1。
读写二进制内容:
size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream);
功能:内存中的数据,以二进制形式写入到文件中。
ptr:要写入的内存的首地址
size:要写入的字节数
nmemb:要写入的次数
stream:文件指针,fopen函数的返回值
返回值:成功写入的次数
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
功能:从文件中以二进制方式读取数据到内存中。
ptr:用来存放数据的内存首地址
size:要读取的字节数
nmemb:要读取的次数
stream:文件指针,fopen函数的返回值
返回值:成功读取的次数
文件位置指针:
每个打开的文件系统都会用一个指针记录着它的读写位置,这个指针指向哪里,
接下来对文件的读取就会从哪里继续,指针的位置会随着文件的读写自动发生变化
文件结构体中有一个成员记录文件的读写位置,称它位文件位置指针,有些情况下需要调整它的位置,获取到正确的数据。
int fseek(FILE *stream, long offset, int whence);
功能:根据基础位置+偏移值调整文件指针的位置。
stream:文件指针,fopen函数的返回值
offset:可以为正负,正往右(偏移值)
whence:(基础位置)
SEEK_SET 文件头
SEEK_CUR 当前位置
SEEK_END 文件尾
long ftell(FILE *stream);
功能:返回文件位置指针所在的位置。
void rewind(FILE *stream);
功能:把文件位置指针调整到开头
文件关闭:
int fclose(FILE *fp);
功能:把文件关闭,以释放相关资源,避免数据丢失。
多文件编程:
随着代码量的增加,不得不把代码分成若干个.c文件编写,这样便于代码重用、代码编译和代码管理。
但缺点是不方便编译,需要借助编译脚本。
如何进行多文件编译:根据功能、责任分成若干个.c文件,然后为每个.c文件配备一个辅助文件.h然后单独编译每个.c文件,
生成目标文件.o,然后再把.o文件合并成可执行文件。
头文件中应该写什么:
1、头文件卫士
2、宏常量、宏函数
3、结构、联合、枚举的设计
4、变量、函数的声明
5、static函数的实现
编译脚本:
把用于编译的命令记录到文件中(makefile/Makefile),在终端里执行make程序时,make程序会自动读取当前目录中的。。。
make程序会监控每个文件的最后修改时间,如果没有被修改的文件不需要重新编译,这样可以节约大量的时间
注意:一定要使用tab缩进
GDB调试:
1、设置ubuntu系统,当段错误时产生core
ulimit -c unlimited
2、编译时增加-g参数
3、再次执行新编译的程序,重新产生core文件
4、gdb a.out core 进行调试
run/where