C/C++总结

198 阅读42分钟

#内存分区 数据类型

数据类型基本概念:

  • 型是对数据的抽象;
  • 类型相同的数据具有相同的表示形式、存储格式以及相关操作;
  • 程序中所有的数据都必定属于某种数据类型;
  • 数据类型可以理解为创建变量的模具: 固定大小内存的别名; 数据类型别名
typedef unsigned int u32;
typedef struct _PERSON{
 char name[64];
 int age;
}Person;

void test(){
 u32 val; //相当于 unsigned int val;
 Person person; //相当于 struct PERSON person;
}

void数据类型

void字面意思是”无类型”,void* 无类型指针,无类型指针可以指向任何类型的数据。

void定义变量是没有任何意义的,当你定义void a,编译器会报错。

void真正用在以下两个方面:

  • 对函数返回的限定;
  • 对函数参数的限定;
//1. void修饰函数参数和函数返回
void test01(void){
 printf("hello world");
}

//2. 不能定义void类型变量
void test02(){
 void val; //报错
}

//3. void* 可以指向任何类型的数据,被称为万能指针
void test03(){
 int a = 10;
 void* p = NULL;
 p = &a;
 printf("a:%d\n",*(int*)p);
 
 char c = 'a';
 p = &c;
 printf("c:%c\n",*(char*)p);
}

//4. void* 常用于数据类型的封装
void test04(){
 //void * memcpy(void * _Dst, const void * _Src, size_t _Size);
}

sizeof 操作符

sizeof 是 c语言中的一个操作符,类似于++、--等等。sizeof 能够告诉我们编译器为某一特定数据或者某一个类型的数据在内存中分配空间时分配的大小,大小以字节为单位。

基本语法:

sizeof(变量);
sizeof 变量;
sizeof(类型);

sizeof 注意点

  • sizeof返回的占用空间大小是为这个变量开辟的大小,而不只是它用到的空间。和现今住房的建筑面积和实用面积的概念差不多。所以对结构体用的时候,大多情况下就得考虑字节对齐的问题了;
  • sizeof返回的数据结果类型是unsigned int;
  • 要注意数组名和指针变量的区别。通常情况下,我们总觉得数组名和指针变量差不多,但是在用sizeof的时候差别很大,对数组名用sizeof返回的是整个数组的大小,而对指针变量进行操作的时候返回的则是指针变量本身所占得空间,在32位机的条件下一般都是4。而且当数组名作为函数参数时,在函数内部,形参也就是个指针,所以不再返回数组的大小;
//1. sizeof基本用法
void test01(){
 int a = 10;
 printf("len:%d\n", sizeof(a));
 printf("len:%d\n", sizeof(int));
 printf("len:%d\n", sizeof a);
}

//2. sizeof 结果类型
void test02(){
 unsigned int a = 10;
 if (a - 11 < 0){
  printf("结果小于0\n");
 }
 else{
  printf("结果大于0\n");
 }
 int b = 5;
 if (sizeof(b) - 10 < 0){
  printf("结果小于0\n");
 }
 else{
  printf("结果大于0\n");
 }
}

//3. sizeof 碰到数组
void TestArray(int arr[]){
 printf("TestArray arr size:%d\n",sizeof(arr));
}
void test03(){
 int arr[] = { 10, 20, 30, 40, 50 };
 printf("array size: %d\n",sizeof(arr));

 //数组名在某些情况下等价于指针
 int* pArr = arr;
 printf("arr[2]:%d\n",pArr[2]);
 printf("array size: %d\n", sizeof(pArr));

 //数组做函数函数参数,将退化为指针,在函数内部不再返回数组大小
 TestArray(arr);
}

** 数据类型总结**

  • 数据类型本质是固定内存大小的别名,是个模具,C语言规定:通过数据类型定义变量;
  • 数据类型大小计算(sizeof);
  • 可以给已存在的数据类型起别名typedef;
  • 数据类型的封装(void 万能类型); 变量

** 变量的概念**

既能读又能写的内存对象,称为变量;

若一旦初始化后不能修改的对象则称为常量。

变量定义形式: 类型  标识符, 标识符, … , 标识符

变量名的本质

  • 变量名的本质:一段连续内存空间的别名;
  • 程序通过变量来申请和命名内存空间 int a = 0;
  • 通过变量名访问内存空间;
  • 不是向变量名读写数据,而是向变量所代表的内存空间中读写数据;

修改变量的两种方式:

  void test(){
 
 int a = 10;

 //1. 直接修改
 a = 20;
 printf("直接修改,a:%d\n",a);

 //2. 间接修改
 int* p = &a;
 *p = 30;

 printf("间接修改,a:%d\n", a);
}

程序的内存分区模型

内存分区

运行之前 我们要想执行我们编写的c程序,那么第一步需要对这个程序进行编译。 1)预处理:宏定义展开、头文件展开、条件编译,这里并不会检查语法

2)编译:检查语法,将预处理后文件编译生成汇编文件

3)汇编:将汇编文件生成目标文件(二进制文件)

4)链接:将目标文件链接为可执行程序

代码区

存放 CPU 执行的机器指令。通常代码区是可共享的(即另外的执行程序可以调用它),使其可共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可。代码区通常是只读的,使其只读的原因是防止程序意外地修改了它的指t令。另外,代码区还规划了局部变量的相关信息。

全局初始化数据区/静态数据区(data段)

该区包含了在程序中明确被初始化的全局变量、已经初始化的静态变量(包括全局静态变量和t)和常量数据(如字符串常量)。

未初始化数据区(又叫 bss 区)

存入的是全局未初始化变量和未初始化静态变量。未初始化数据区的数据在程序开始执行之前被内核初始化为 0 或者空(NULL)。

总体来讲说,程序源代码被编译之后主要分成两种段:程序指令(代码区)和程序数据(数据区)。代码段属于程序指令,而数据域段和.bss段属于程序数据

2.3.1.1 运行之后

程序在加载到内存前,代码区和全局区(data和bss)的大小就是固定的,程序运行期间不能改变。然后,运行可执行程序,操作系统把物理硬盘程序load(加载)到内存,除了根据可执行程序的信息分出代码区(text)、数据区(data)和未初始化数据区(bss)之外,还额外增加了栈区、堆区。

代码区(text segment)

加载的是可执行文件代码段,所有的可执行代码都加载到代码区,这块内存是不可以在运行期间修改的。

未初始化数据区(BSS)

加载的是可执行文件BSS段,位置可以分开亦可以紧靠数据段,存储于数据段的数据(全局未初始化,静态未初始化数据)的生存周期为整个程序运行过程。

全局初始化数据区/静态数据区(data segment)

加载的是可执行文件数据段,存储于数据段(全局初始化,静态初始化数据,文字常量(只读))的数据的生存周期为整个程序运行过程。

 栈区(stack)

栈是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数值、返回值、局部变量等。在程序运行过程中实时加载和释放,因此,局部变量的生存周期为申请到释放该段栈空间。

 堆区(heap)

堆是一个大容器,它的容量要远远大于栈,但没有栈那样先进后出的顺序。用于动态内存分配。堆在内存中位于BSS区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。

image.png

分区模型

栈区

由系统进行内存的管理。主要存放函数的参数以及局部变量。在函数完成执行,系统自行释放栈区内存,不需要用户管理。

#char* func(){
 char p[] = "hello world!"; //在栈区存储 乱码
 printf("%s\n", p);
 return p;
}
void test(){
 char* p = NULL;
 p = func();  
 printf("%s\n",p); 
}

image.png ** 堆区**

由编程人员手动申请,手动释放,若不手动释放,程序结束后由系统回收,生命周期是整个程序运行期间。使用malloc或者new进行堆的申请。

char* func(){
 char* str = malloc(100);
 strcpy(str, "hello world!");
 printf("%s\n",str);
 return str;
}

void test01(){
 char* p = NULL;
 p = func();
 printf("%s\n",p);
}

void allocateSpace(char* p){
 p = malloc(100);
 strcpy(p, "hello world!");
 printf("%s\n", p);
}

void test02(){
 
 char* p = NULL;
 allocateSpace(p);

 printf("%s\n", p);
}

分区模型

** 栈区**

由系统进行内存的管理。主要存放函数的参数以及局部变量。在函数完成执行,系统自行释放栈区内存,不需要用户管理。

#char* func(){
 char p[] = "hello world!"; //在栈区存储 乱码
 printf("%s\n", p);
 return p;
}
void test(){
 char* p = NULL;
 p = func();  
 printf("%s\n",p); 
}

** 堆区**

由编程人员手动申请,手动释放,若不手动释放,程序结束后由系统回收,生命周期是整个程序运行期间。使用malloc或者new进行堆的申请。

char* func(){
 char* str = malloc(100);
 strcpy(str, "hello world!");
 printf("%s\n",str);
 return str;
}

void test01(){
 char* p = NULL;
 p = func();
 printf("%s\n",p);
}

void allocateSpace(char* p){
 p = malloc(100);
 strcpy(p, "hello world!");
 printf("%s\n", p);
}

void test02(){
 
 char* p = NULL;
 allocateSpace(p);

 printf("%s\n", p);
}

image.png

堆分配内存API:

#include <stdlib.h>
void *calloc(size_t nmemb, size_t size);

功能:

在内存动态存储区中分配nmemb块长度为size字节的连续区域。calloc自动将分配的内存 置0。

参数:

nmemb:所需内存单元数量 size:每个内存单元的大小(单位:字节)

返回值:

成功:分配空间的起始地址

失败:NULL

#include <stdlib.h>
void *realloc(void *ptr, size_t size);

功能:

重新分配用malloc或者calloc函数在堆中分配内存空间的大小。 realloc不会自动清理增加的内存,需要手动清理,如果指定的地址后面有连续的空间,那么就会在已有地址基础上增加内存,如果指定的地址后面没有空间,那么realloc会重新分配新的连续内存,把旧内存的值拷贝到新内存,同时释放旧内存。

参数:

ptr:为之前用malloc或者calloc分配的内存地址,如果此参数等于NULL,那么和realloc与malloc功能一致

size:为重新分配内存的大小, 单位:字节

返回值:

成功:新分配的堆内存地址

失败:NULL

void test01(){
 
 int* p1 = calloc(10,sizeof(int));
 if (p1 == NULL){
  return;
 }
 for (int i = 0; i < 10; i ++){
  p1[i] = i + 1;
 }
 for (int i = 0; i < 10; i++){
  printf("%d ",p1[i]);
 }
 printf("\n");
 free(p1);
}

void test02(){
 int* p1 = calloc(10, sizeof(int));
 if (p1 == NULL){
  return;
 }
 for (int i = 0; i < 10; i++){
  p1[i] = i + 1;
 }

 int* p2 = realloc(p1, 15 * sizeof(int));
 if (p2 == NULL){
  return;
 }

 printf("%d\n", p1);
 printf("%d\n", p2);

 //打印
 for (int i = 0; i < 15; i++){
  printf("%d ", p2[i]);
 }
 printf("\n");

 //重新赋值
 for (int i = 0; i < 15; i++){
  p2[i] = i + 1;
 }
 
 //再次打印
 for (int i = 0; i < 15; i++){
  printf("%d ", p2[i]);
 }
 printf("\n");

 free(p2);
}

全局/静态区

全局静态区内的变量在编译阶段已经分配好内存空间并初始化。这块内存在程序运行期间一直存在,它主要存储全局变量、静态变量和常量。

注意:

(1)这里不区分初始化和未初始化的数据区,是因为静态存储区内的变量若不显示初始化,则编译器会自动以默认的方式进行初始化,即静态存储区内不存在未初始化的变量。

(2)全局静态存储区内的常量分为常变量和字符串常量,一经初始化,不可修改。静态存储内的常变量是全局变量,与局部常变量不同,区别在于局部常变量存放于栈,实际可间接通过指针或者引用进行修改,而全局常变量存放于静态常量区则不可以间接修改。

(3)字符串常量存储在全局/静态存储区的常量区。

int v1 = 10;//全局/静态区
const int v2 = 20; //常量,一旦初始化,不可修改
static int v3 = 20; //全局/静态区
char *p1; //全局/静态区,编译器默认初始化为NULL

//那么全局static int 和 全局int变量有什么区别?

void test(){
 static int v4 = 20; //全局/静态区
}
char* func(){
 static char arr[] = "hello world!"; //在静态区存储 可读可写
 arr[2] = 'c';
 char* p = "hello world!"; //全局/静态区-字符串常量区 
 //p[2] = 'c'; //只读,不可修改 
 printf("%d\n",arr);
 printf("%d\n",p);
 printf("%s\n", arr);
 return arr;
}
void test(){
 char* p = func();
 printf("%s\n",p);
}

** 总结**

在理解C/C++内存分区时,常会碰到如下术语:数据区,堆,栈,静态区,常量区,全局区,字符串常量区,文字常量区,代码区等等,初学者被搞得云里雾里。在这里,尝试捋清楚以上分区的关系。

数据区包括:堆,栈,全局/静态存储区。

  • 全局/静态存储区包括:常量区,全局区、静态区。
  • 常量区包括:字符串常量区、常变量区。
  • 代码区:存放程序编译后的二进制代码,不可寻址区。

可以说,C/C++内存分区其实只有两个,即代码区和数据区。

2.3.3 函数调用模型

2.3.3.1 函数调用流程

栈(stack)是现代计算机程序里最为重要的概念之一,几乎每一个程序都使用了栈,没有栈就没有函数,没有局部变量,也就没有我们如今能见到的所有计算机的语言。在解释为什么栈如此重要之前,我们先了解一下传统的栈的定义:

在经典的计算机科学中,栈被定义为一个特殊的容器,用户可以将数据压入栈中(入栈,push),也可以将压入栈中的数据弹出(出栈,pop),但是栈容器必须遵循一条规则:先入栈的数据最后出栈(First In Last Out,FILO).

在经典的操作系统中,栈总是向下增长的。压栈的操作使得栈顶的地址减小,弹出操作使得栈顶地址增大。

栈在程序运行中具有极其重要的地位。最重要的,栈保存一个函数调用所需要维护的信息,这通常被称为堆栈帧(Stack Frame)或者活动记录(Activate Record).一个函数调用过程所需要的信息一般包括以下几个方面:

  • 函数的返回地址;
  • 函数的参数;
  • 临时变量;
  • 保存的上下文:包括在函数调用前后需要保持不变的寄存器。

我们从下面的代码,分析以下函数的调用过程:

int func(int a,int b){
 int t_a = a;
 int t_b = b;
 return t_a + t_b;
}

int main(){
 int ret = 0;
 ret = func(10, 20);
 return EXIT_SUCCESS;
}

image.png

调用惯例

现在,我们大致了解了函数调用的过程,这期间有一个现象,那就是函数的调用者和被调用者对函数调用有着一致的理解,例如,它们双方都一致的认为函数的参数是按照某个固定的方式压入栈中。如果不这样的话,函数将无法正确运行。

如果函数调用方在传递参数的时候先压入a参数,再压入b参数,而被调用函数则认为先压入的是b,后压入的是a,那么被调用函数在使用a,b值时候,就会颠倒。

因此,函数的调用方和被调用方对于函数是如何调用的必须有一个明确的约定,只有双方都遵循同样的约定,函数才能够被正确的调用,这样的约定被称为”调用惯例(Calling Convention)”.一个调用惯例一般包含以下几个方面:

函数参数的传递顺序和方式

函数的传递有很多种方式,最常见的是通过栈传递。函数的调用方将参数压入栈中,函数自己再从栈中将参数取出。对于有多个参数的函数,调用惯例要规定函数调用方将参数压栈的顺序:从左向右,还是从右向左。有些调用惯例还允许使用寄存器传递参数,以提高性能。

栈的维护方式

在函数将参数压入栈中之后,函数体会被调用,此后需要将被压入栈中的参数全部弹出,以使得栈在函数调用前后保持一致。这个弹出的工作可以由函数的调用方来完成,也可以由函数本身来完成。

为了在链接的时候对调用惯例进行区分,调用惯例要对函数本身的名字进行修饰。不同的调用惯例有不同的名字修饰策略。

事实上,在c语言里,存在着多个调用惯例,而默认的是cdecl.任何一个没有显示指定调用惯例的函数都是默认是cdecl惯例。比如我们上面对于func函数的声明,它的完整写法应该是:

int _cdecl func(int a,int b);

注意: cdecl不是标准的关键字,在不同的编译器里可能有不同的写法,例如gcc里就不存在_cdecl这样的关键字,而是使用__attribute_((cdecl)).

image.png

image.png

** 函数变量传递分析**

image.png

image.png

栈的生长方向和内存存放方向

//1. 栈的生长方向
void test01(){
int a = 10;
int b = 20;
int c = 30;
int d = 40;
printf("a = %d\n", &a);
printf("b = %d\n", &b);
printf("c = %d\n", &c);
printf("d = %d\n", &d);
//a的地址大于b的地址,故而生长方向向下
}
//2. 内存生长方向(小端模式)
void test02(){
//高位字节 -> 地位字节
int num = 0xaabbccdd;
unsigned char* p = #
//从首地址开始的第一个字节
printf("%x\n",*p);
printf("%x\n", *(p + 1));
printf("%x\n", *(p + 2));
printf("%x\n", *(p + 3));
}

为什么使用指针

假如我们定义了 char a=’A’ ,当需要使用 ‘A’ 时,除了直接调用变量 a ,还可以定义 char *p=&a ,调用 a 的地址,即指向 a 的指针 p ,变量 a( char 类型)只占了一个字节,指针本身的大小由可寻址的字长来决定,指针 p 占用 4 个字节。

但如果要引用的是占用内存空间比较大东西,用指针也还是 4 个字节即可。

  • 数据传递时,如果数据块较大,可以使用指针传递地址而不是实际数据,即提高传输速度,又节省大量内存。

一个数据缓冲区 char buf[100] ,如果其中 buf[0,1] 为命令号, buf[2,3] 为数据类型, buf[4~7] 为该类型的数值,类型为 int ,使用如下语句进行赋值:

*(short*)&buf[0]=DataId;
*(short*)&buf[2]=DataType;
*(int*)&buf[4]=DataValue;
  • 数据转换,利用指针的灵活的类型转换,可以用来做数据类型转换,比较常用于通讯缓冲区的填充。
  • 指针的机制比较简单,其功能可以被集中重新实现成更抽象化的引用数据形式
  • 函数指针,形如: #define PMYFUN (void*)(int,int) ,可以用在大量分支处理的实例当中,如某通讯根据不同的命令号执行不同类型的命令,则可以建立一个函数指针数组,进行散转。
  • 在数据结构中,链表、树、图等大量的应用都离不开指针。

指针强化

指针是一种数据类型

操作系统将硬件和软件结合起来,给程序员提供的一种对内存使用的抽象,这种抽象机制使得程序使用的是虚拟存储器,而不是直接操作和使用真实存在的物理存储器。所有的虚拟地址形成的集合就是虚拟地址空间。

内存是一个很大的线性的字节数组,每个字节固定由 8 个二进制位组成,每个字节都有唯一的编号,如下图,这是一个 4G 的内存,他一共有 4x1024x1024x1024 = 4294967296 个字节,那么它的地址范围就是 0 ~ 4294967296 ,十六进制表示就是 0x00000000~0xffffffff ,当程序使用的数据载入内存时,都有自己唯一的一个编号,这个编号就是这个数据的地址。指针就是这样形成的。

1.1.1 指针变量

指针是一种数据类型,占用内存空间,用来保存内存地址。

void test01(){
 
 int* p1 = 0x1234;
 int*** p2 = 0x1111;

 printf("p1 size:%d\n",sizeof(p1));
 printf("p2 size:%d\n",sizeof(p2));


 //指针是变量,指针本身也占内存空间,指针也可以被赋值
 int a = 10;
 p1 = &a;

 printf("p1 address:%p\n", &p1);
 printf("p1 address:%p\n", p1);
 printf("a address:%p\n", &a);

}

野指针和空指针

空指针

标准定义了NULL指针,它作为一个特殊的指针变量,表示不指向任何东西。要使一个指针为NULL,可以给它赋值一个零值。为了测试一个指针百年来那个是否为NULL,你可以将它与零值进行比较。

对指针解引用操作可以获得它所指向的值。但从定义上看,NULL指针并未指向任何东西,因为对一个NULL指针因引用是一个非法的操作,在解引用之前,必须确保它不是一个NULL指针。

如果对一个NULL指针间接访问会发生什么呢?结果因编译器而异。 不允许向NULL和非法地址拷贝内存:

void test(){
 char *p = NULL;
 //给p指向的内存区域拷贝内容
 strcpy(p, "1111"); //err

 char *q = 0x1122;
 //给q指向的内存区域拷贝内容
 strcpy(q, "2222"); //err  
}

野指针

在使用指针时,要避免野指针的出现:

野指针指向一个已删除的对象或未申请访问受限内存区域的指针。与空指针不同,野指针无法通过简单地判断是否为 NULL避免,而只能通过养成良好的编程习惯来尽力减少。对野指针进行操作很容易造成程序错误。

什么情况下会导致野指针?

  • 指针变量未初始化

任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。

  • 指针释放后未置空

有时指针在free或delete后未赋值 NULL,便会使人以为是合法的。别看free和delete的名字(尤其是delete),它们只是把指针所指的内存给释放掉,但并没有把指针本身干掉。此时指针指向的就是“垃圾”内存。释放后的指针应立即将指针置为NULL,防止产生“野指针”。

  • 指针操作超越变量作用域

不要返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放。

void test(){
 int* p = 0x001; //未初始化
 printf("%p\n",p);
 *p = 100;
}

操作野指针是非常危险的操作,应该规避野指针的出现:

  • 初始化时置 NULL

指针变量一定要初始化为NULL,因为任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的。

  • 释放时置 NULL

当指针p指向的内存空间释放时,没有设置指针p的值为NULL。delete和free只是把内存空间释放了,但是并没有将指针p的值赋为NULL。通常判断一个指针是否合法,都是使用if语句测试该指针是否为NULL。

void*类型指针

void是一种特殊的指针类型,可以用来存放任意对象的地址。一个void指针存放着一个地址,这一点和其他指针类似。不同的是,我们对它到底储存的是什么对象的地址并不了解。

double a=2.3;
int b=5;
void *p=&a;
cout<<p<<endl;   //输出了a的地址

p=&b;
cout<<p<<endl;   //输出了b的地址

//cout<<*p<<endl;这一行不可以执行,void*指针只可以储存变量地址,不可以直接操作它指向的对象

由于void是空类型,只保存了指针的值,而丢失了类型信息,我们不知道他指向的数据是什么类型的,只指定这个数据在内存中的起始地址,如果想要完整的提取指向的数据,程序员就必须对这个指针做出正确的类型转换,然后再解指针。

void*数组和指针

  • 同类型指针变量可以相互赋值,数组不行,只能一个一个元素的赋值或拷贝
  • 数组在内存中是连续存放的,开辟一块连续的内存空间。数组是根据数组的下进行访问的。指针很灵活,它可以指向任意类型的数据。指针的类型说明了它所指向地址空间的内存。
  • 数组所占存储空间的内存:sizeof(数组名) 数组的大小:sizeof(数组名)/sizeof(数据类型),在32位平台下,无论指针的类型是什么,sizeof(指针名)都是 4 ,在 64 位平台下,无论指针的类型是什么,sizeof(指针名)都是 8 。
  • 数组名作为右值的时候,就是第一个元素的地址
int main(void)
{
    int arr[5] = {1,2,3,4,5};

    int *p_first = arr;
    printf("%d",*p_first);  //1
    return 0;
}
  • 指向数组元素的指针 支持 递增 递减 运算。p= p+1意思是,让p指向原来指向的内存块的下一个相邻的相同类型的内存块。在数组中相邻内存就是相邻下标元素。

1间接访问操作符

通过一个指针访问它所指向的地址的过程叫做间接访问,或者叫解引用指针,这个用于执行间接访问的操作符是*。

注意:对一个int类型指针解引用会产生一个整型值,类似地,对一个float指针解引用会产生了一个float类型的值。

int arr[5];
int *p = * (&arr);
int arr1[5][3] arr1 = int(*)[3]&arr1

1)在指针声明时,* 号表示所声明的变量为指针

2)在指针使用时,* 号表示操作指针所指向的内存空间

  • *相当通过地址(指针变量的值)找到指针指向的内存,再操作内存
  • *放在等号的左边赋值(给内存赋值,写内存)
  • *放在等号的右边取值(从内存中取值,读内存)
//解引用
void test01(){

 //定义指针
 int* p = NULL;
 //指针指向谁,就把谁的地址赋给指针
 int a = 10;
 p = &a;
 *p = 20;//*在左边当左值,必须确保内存可写
 //*号放右面,从内存中读值
 int b = *p;
 //必须确保内存可写
 char* str = "hello world!";
 *str = 'm';

 printf("a:%d\n", a);
 printf("*p:%d\n", *p);
 printf("b:%d\n", b);
}

指针的步长

指针是一种数据类型,是指它指向的内存空间的数据类型。指针所指向的内存空间决定了指针的步长。指针的步长指的是,当指针+1时候,移动多少字节单位。

思考如下问题:

int a = 0xaabbccdd;
unsigned int *p1 = &a;
unsigned char *p2 = &a;

//为什么*p1打印出来正确结果?
printf("%x\n", *p1);
//为什么*p2没有打印出来正确结果?
printf("%x\n", *p2);

//为什么p1指针+1加了4字节?
printf("p1  =%d\n", p1);
printf("p1+1=%d\n", p1 + 1);
//为什么p2指针+1加了1字节?
printf("p2  =%d\n", p2);
printf("p2+1=%d\n", p2 + 1);

函数与指针

函数的参数和指针

C语言中,实参传递给形参,是按值传递的,也就是说,函数中的形参是实参的拷贝份,形参和实参只是在值上面一样,而不是同一个内存数据对象。这就意味着:这种数据传递是单向的,即从调用者传递给被调函数,而被调函数无法修改传递的参数达到回传的效果。

void change(int a)
{
    a++;      //在函数中改变的只是这个函数的局部变量a,而随着函数执行结束,a被销毁。age还是原来的age,纹丝不动。
}
int main(void)
{
    int age = 60;
    change(age);
    printf("age = %d",age);   // age = 60
    return 0;
}

有时候我们可以使用函数的返回值来回传数据,在简单的情况下是可以的,但是如果返回值有其它用途(例如返回函数的执行状态量),或者要回传的数据不止一个,返回值就解决不了了。

传递变量的指针可以轻松解决上述问题。

void change(int* pa)
{
    (*pa)++;   //因为传递的是age的地址,因此pa指向内存数据age。当在函数中对指针pa解地址时,
               //会直接去内存中找到age这个数据,然后把它增1。
}
int main(void)
{
    int age = 160;
    change(&age);
    printf("age = %d",age);   // age = 61
    return 0;
}

比如指针的一个常见的使用例子:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void swap(int *,int *);
int main()
{
    int a=5,b=10;
    printf("a=%d,b=%d\n",a,b);
    swap(&a,&b);
    printf("a=%d,b=%d\n",a,b);
    return 0;
}
void swap(int *pa,int *pb)
{
    int t=*pa;*pa=*pb;*pb=t;
}

在以上的例子中,swap函数的两个形参pa和pb可以接收两个整型变量的地址,并通过间接访问的方式修改了它指向变量的值。在main函数中调用swap时,提供的实参分别为&a,&b,这样就实现了pa=&a,pb=&b的赋值过程,这样在swap函数中就通过pa修改了 a 的值,通过pb修改了 b 的值。因此,如果需要在被调函数中修改主调函数中变量的值,就需要经过以下几个步骤:

  • 定义函数的形参必须为指针类型,以接收主调函数中传来的变量的地址;
  • 调用函数时实参为变量的地址;
  • 在被调函数中使用*间接访问形参指向的内存空间,实现修改主调函数中变量值的功能。

指针作为函数的形参的另一个典型应用是当函数有多个返回值的情形。比如,需要在一个函数中统计一个数组的最大值、最小值和平均值。当然你可以编写三个函数分别完成统计三个值的功能。但比较啰嗦,如:

int GetMax(int a[],int n)
{
    int max=a[0],i;
    for(i=1;i<n;i++)
    {
        if(max<a[i]) max=a[i];
    }
    return max;
}
int GetMin(int a[],int n)
{
    int min=a[0],i;
    for(i=1;i<n;i++)
    {
        if(min>a[i]) min=a[i];
    }
    return min;
}
double GetAvg(int a[],int n)
{
    double avg=0;
    int i;
    for(i=0;i<n;i++)
    {
        avg+=a[i];
    }
    return avg/n;
}

其实我们完全可以在一个函数中完成这个功能,由于函数只能有一个返回值,可以返回平均值,最大值和最小值可以通过指针类型的形参来进行实现:

double Stat(int a[],int n,int *pmax,int *pmin)
{
    double avg=a[0];
    int i;
    *pmax=*pmin=a[0];
    for(i=1;i<n;i++)
    {
        avg+=a[i];
        if(*pmax<a[i]) *pmax=a[i];
        if(*pmin>a[i]) *pmin=a[i];
    }
    return avg/n;
}

函数的指针

一个函数总是占用一段连续的内存区域,函数名在表达式中有时也会被转换为该函数所在内存区域的首地址。我们可以把函数的这个首地址赋予一个指针变量,使指针变量指向函数所在的内存区域,然后通过指针变量就可以找到并调用该函数。这种指针就是函数指针。

函数指针的定义形式为:

returnType (*pointerName)(param list);

returnType 为函数返回值类型,pointerNmae 为指针名称,param list 为函数参数列表。参数列表中可以同时给出参数的类型和名称,也可以只给出参数的类型,省略参数的名称,这一点和函数原型非常类似。

用指针来实现对函数的调用:

#include <stdio.h>
//返回两个数中较大的一个
int max(int a, int b)
{
    return a>b ? a : b;
}
int main()
{
    int x, y, maxval;
    //定义函数指针
    int (*pmax)(int, int) = max;  //也可以写作int (*pmax)(int a, int b)
    printf("Input two numbers:");
    scanf("%d %d", &x, &y);
    maxval = (*pmax)(x, y);
    printf("Max value: %d\n", maxval);
    return 0;
}

结构体和指针

结构体指针有特殊的语法: -> 符号

如果p是一个结构体指针,则可以使用 p ->【成员】 的方法访问结构体的成员


typedef struct
{
    char name[31];
    int age;
    float score;
}Student;

int main(void)
{
    Student stu = {"Bob" , 19, 98.0};
    Student*ps = &stu;

    ps->age = 20;
    ps->score = 99.0;
    printf("name:%s age:%d
",ps->name,ps->age);
    return 0;
}

指针的意义_间接赋值

间接赋值的三大条件

通过指针间接赋值成立的三大条件:

  • 2个变量(一个普通变量一个指针变量、或者一个实参一个形参)
  • 建立关系
  • 通过 * 操作指针指向的内存
void test(){
 int a = 100; //两个变量
 int *p = NULL;
 //建立关系
 //指针指向谁,就把谁的地址赋值给指针
 p = &a;
 //通过*操作内存
 *p = 22;
}

如何定义合适的指针变量

void test(){
 int b;  
 int *q = &b; //0级指针
 int **t = &q;
 int ***m = &t;
}

间接赋值:从0级指针到1级指针

int func1(){ return 10; }

void func2(int a){
 a = 100;
}
//指针的意义_间接赋值
void test02(){
 int a = 0;
 a = func1();
 printf("a = %d\n", a);

 //为什么没有修改?
 func2(a);
 printf("a = %d\n", a);
}

//指针的间接赋值
void func3(int* a){
 *a = 100;
}

void test03(){
 int a = 0;
 a = func1();
 printf("a = %d\n", a);

 //修改
 func3(&a);
 printf("a = %d\n", a);
}

间接赋值:从1级指针到2级指针

void AllocateSpace(char** p){
 *p = (char*)malloc(100);
 strcpy(*p, "hello world!");
}

void FreeSpace(char** p){

 if (p == NULL){
  return;
 }
 if (*p != NULL){
  free(*p);
  *p = NULL;
 }

}

void test(){
 
 char* p = NULL;

 AllocateSpace(&p);
 printf("%s\n",p);
 FreeSpace(&p);

 if (p == NULL){
  printf("p内存释放!\n");
 }
}

间接赋值的推论

  • 用1级指针形参,去间接修改了0级指针(实参)的值。
  • 用2级指针形参,去间接修改了1级指针(实参)的值。
  • 用3级指针形参,去间接修改了2级指针(实参)的值。
  • 用n级指针形参,去间接修改了n-1级指针(实参)的值。

指针做函数参数

指针做函数参数,具备输入和输出特性:

  • 输入:主调函数分配内存
  • 输出:被调用函数分配内存

输入特性

void fun(char *p /* in */)
{
 //给p指向的内存区域拷贝内容
 strcpy(p, "abcddsgsd");
}

void test(void)
{
 //输入,主调函数分配内存
 char buf[100] = { 0 };
 fun(buf);
 printf("buf  = %s\n", buf);
}

输出特性

void fun(char **p /* out */, int *len)
{
 char *tmp = (char *)malloc(100);
 if (tmp == NULL)
 {
  return;
 }
 strcpy(tmp, "adlsgjldsk");

 //间接赋值
 *p = tmp;
 *len = strlen(tmp);
}

void test(void)
{
 //输出,被调用函数分配内存,地址传递
 char *p = NULL;
 int len = 0;
 fun(&p, &len);
 if (p != NULL)
 {
  printf("p = %s, len = %d\n", p, len);
 }
}

#字符串指针强化

字符串指针做函数参数

字符串基本操作

//字符串基本操作
//字符串是以0或者'\0'结尾的字符数组,(数字0和字符'\0'等价)
void test01(){

 //字符数组只能初始化5个字符,当输出的时候,从开始位置直到找到0结束
 char str1[] = { 'h', 'e', 'l', 'l', 'o' };
 printf("%s\n",str1);

 //字符数组部分初始化,剩余填0
 char str2[100] = { 'h', 'e', 'l', 'l', 'o' };
 printf("%s\n", str2);

 //如果以字符串初始化,那么编译器默认会在字符串尾部添加'\0'
 char str3[] = "hello";
 printf("%s\n",str3);
 printf("sizeof str:%d\n",sizeof(str3));
 printf("strlen str:%d\n",strlen(str3));

 //sizeof计算数组大小,数组包含'\0'字符
 //strlen计算字符串的长度,到'\0'结束

 //那么如果我这么写,结果是多少呢?
 char str4[100] = "hello";
 printf("sizeof str:%d\n", sizeof(str4));
 printf("strlen str:%d\n", strlen(str4));

 //请问下面输入结果是多少?sizeof结果是多少?strlen结果是多少?
 char str5[] = "hello\0world"; 
 printf("%s\n",str5);
 printf("sizeof str5:%d\n",sizeof(str5));
 printf("strlen str5:%d\n",strlen(str5));

 //再请问下面输入结果是多少?sizeof结果是多少?strlen结果是多少?
 char str6[] = "hello\012world";
 printf("%s\n", str6);
 printf("sizeof str6:%d\n", sizeof(str6));
 printf("strlen str6:%d\n", strlen(str6));
}

八进制和十六进制转义字符:

在C中有两种特殊的字符,八进制转义字符和十六进制转义字符,八进制字符的一般形式是'\ddd',d是0-7的数字。十六进制字符的一般形式是'\xhh',h是0-9或A-F内的一个。八进制字符和十六进制字符表示的是字符的ASCII码对应的数值。

比如 :

  • '\063'表示的是字符'3',因为'3'的ASCII码是30(十六进制),48(十进制),63(八进制)。
  • '\x41'表示的是字符'A',因为'A'的ASCII码是41(十六进制),65(十进制),101(八进制)。

字符串拷贝功能实现

//拷贝方法1
void copy_string01(char* dest, char* source ){

 for (int i = 0; source[i] != '\0';i++){
  dest[i] = source[i];
 }

}

//拷贝方法2
void copy_string02(char* dest, char* source){
 while (*source != '\0' /* *source != 0 */){
  *dest = *source;
  source++;
  dest++;
 }
}

//拷贝方法3
void copy_string03(char* dest, char* source){
 //判断*dest是否为0,0则退出循环
 while (*dest++ = *source++){}
}

字符串反转模型

image.png

void reverse_string(char* str){

 if (str == NULL){
  return;
 }

 int begin = 0;
 int end = strlen(str) - 1;
 
 while (begin < end){
  
  //交换两个字符元素
  char temp = str[begin];
  str[begin] = str[end];
  str[end] = temp;

  begin++;
  end--;
 }

}

void test(){
 char str[] = "abcdefghijklmn";
 printf("str:%s\n", str);
 reverse_string(str);
 printf("str:%s\n", str);
}

字符串的格式化

sprintf

#include <stdio.h>
int sprintf(char *str, const char *format, ...);

功能:根据参数format字符串来转换并格式化数据,然后将结果输出到str指定的空间中,直到 出现字符串结束符 '\0' 为止。

参数

  • str:字符串首地址
  • format:字符串格式,用法和printf()一样

返回值

  • 成功:实际格式化的字符个数
  • 失败: - 1
void test(){
 
 //1. 格式化字符串
 char buf[1024] = { 0 };
 sprintf(buf, "你好,%s,欢迎加入我们!", "John");
 printf("buf:%s\n",buf);

 memset(buf, 0, 1024);
 sprintf(buf, "我今年%d岁了!", 20);
 printf("buf:%s\n", buf);

 //2. 拼接字符串
 memset(buf, 0, 1024);
 char str1[] = "hello";
 char str2[] = "world";
 int len = sprintf(buf,"%s %s",str1,str2);
 printf("buf:%s len:%d\n", buf,len);

 //3. 数字转字符串
 memset(buf, 0, 1024);
 int num = 100;
 sprintf(buf, "%d", num);
 printf("buf:%s\n", buf);
 //设置宽度 右对齐
 memset(buf, 0, 1024);
 sprintf(buf, "%8d", num);
 printf("buf:%s\n", buf);
 //设置宽度 左对齐
 memset(buf, 0, 1024);
 sprintf(buf, "%-8d", num);
 printf("buf:%s\n", buf);
 //转成16进制字符串 小写
 memset(buf, 0, 1024);
 sprintf(buf, "0x%x", num);
 printf("buf:%s\n", buf);

 //转成8进制字符串
 memset(buf, 0, 1024);
 sprintf(buf, "0%o", num);
 printf("buf:%s\n", buf);
}

1sscanf

#include <stdio.h>
int sscanf(const char *str, const char *format, ...);

功能:从str指定的字符串读取数据,并根据参数format字符串来转换并格式化数据。

参数

  • str:指定的字符串首地址
  • format:字符串格式,用法和scanf()一样

返回值

  • 成功:成功则返回参数数目,失败则返回-1
  • 失败: - 1
//1. 跳过数据
void test01(){
 char buf[1024] = { 0 };
 //跳过前面的数字
 //匹配第一个字符是否是数字,如果是,则跳过
 //如果不是则停止匹配
 sscanf("123456aaaa", "%*d%s", buf); 
 printf("buf:%s\n",buf);
}

//2. 读取指定宽度数据
void test02(){
 char buf[1024] = { 0 };
 //跳过前面的数字
 sscanf("123456aaaa", "%7s", buf);
 printf("buf:%s\n", buf);
}

//3. 匹配a-z中任意字符
void test03(){
 char buf[1024] = { 0 };
 //跳过前面的数字
 //先匹配第一个字符,判断字符是否是a-z中的字符,如果是匹配
 //如果不是停止匹配
 sscanf("abcdefg123456", "%[a-z]", buf);
 printf("buf:%s\n", buf);
}

//4. 匹配aBc中的任何一个
void test04(){
 char buf[1024] = { 0 };
 //跳过前面的数字
 //先匹配第一个字符是否是aBc中的一个,如果是,则匹配,如果不是则停止匹配
 sscanf("abcdefg123456", "%[aBc]", buf);
 printf("buf:%s\n", buf);
}

//5. 匹配非a的任意字符
void test05(){
 char buf[1024] = { 0 };
 //跳过前面的数字
 //先匹配第一个字符是否是aBc中的一个,如果是,则匹配,如果不是则停止匹配
 sscanf("bcdefag123456", "%[^a]", buf);
 printf("buf:%s\n", buf);
}

//6. 匹配非a-z中的任意字符
void test06(){
 char buf[1024] = { 0 };
 //跳过前面的数字
 //先匹配第一个字符是否是aBc中的一个,如果是,则匹配,如果不是则停止匹配
 sscanf("123456ABCDbcdefag", "%[^a-z]", buf);
 printf("buf:%s\n", buf);
}

一级指针易错点

越界

void test(){
 char buf[3] = "abc";
 printf("buf:%s\n",buf);
}

指针叠加会不断改变指针指向

void test(){
 char *p = (char *)malloc(50);
 char buf[] = "abcdef";
 int n = strlen(buf);
 int i = 0;

 for (i = 0; i < n; i++)
 {
  *p = buf[i];
  p++; //修改原指针指向
 }

 free(p);
}

返回局部变量地址

char *get_str()
{
 char str[] = "abcdedsgads"; //栈区,
 printf("[get_str]str = %s\n", str);
 return str;
}

同一块内存释放多次(不可以释放野指针)

void test(){ 
 char *p = NULL;

 p = (char *)malloc(50);
 strcpy(p, "abcdef");

 if (p != NULL)
 {
  //free()函数的功能只是告诉系统 p 指向的内存可以回收了
  // 就是说,p 指向的内存使用权交还给系统
  //但是,p的值还是原来的值(野指针),p还是指向原来的内存
  free(p); 
 }

 if (p != NULL)
 {
  free(p);
 }
}

const使用

//const修饰变量
void test01(){
 //1. const基本概念
 const int i = 0;
 //i = 100; //错误,只读变量初始化之后不能修改

 //2. 定义const变量最好初始化
 const int j;
 //j = 100; //错误,不能再次赋值

 //3. c语言的const是一个只读变量,并不是一个常量,可通过指针间接修改
 const int k = 10;
 //k = 100; //错误,不可直接修改,我们可通过指针间接修改
 printf("k:%d\n", k);
 int* p = &k;
 *p = 100;
 printf("k:%d\n", k);
}

//const 修饰指针
void test02(){

 int a = 10;
 int b = 20;
 //const放在*号左侧 修饰p_a指针指向的内存空间不能修改,但可修改指针的指向
 const int* p_a = &a;
 //*p_a = 100; //不可修改指针指向的内存空间
 p_a = &b; //可修改指针的指向

 //const放在*号的右侧, 修饰指针的指向不能修改,但是可修改指针指向的内存空间
 int* const p_b = &a;
 //p_b = &b; //不可修改指针的指向
 *p_b = 100; //可修改指针指向的内存空间

 //指针的指向和指针指向的内存空间都不能修改
 const int* const p_c = &a;
}
//const指针用法
struct Person{
 char name[64];
 int id;
 int age;
 int score;
};

//每次都对对象进行拷贝,效率低,应该用指针
void printPersonByValue(struct Person person){
 printf("Name:%s\n", person.name);
 printf("Name:%d\n", person.id);
 printf("Name:%d\n", person.age);
 printf("Name:%d\n", person.score);
}

//但是用指针会有副作用,可能会不小心修改原数据
void printPersonByPointer(const struct Person *person){
 printf("Name:%s\n", person->name);
 printf("Name:%d\n", person->id);
 printf("Name:%d\n", person->age);
 printf("Name:%d\n", person->score);
}
void test03(){
 struct Person p = { "Obama", 1101, 23, 87 };
 //printPersonByValue(p);
 printPersonByPointer(&p);
}

指针的指针(二级指针)

#二级指针基本概念

这里让我们花点时间来看一个例子,揭开这个即将开始的序幕。考虑下面这些声明:

int a = 12;
int *b = &a;

它们如下图进行内存分配:

image.png 假定我们又有了第3个变量,名叫c,并用下面这条语句对它进行初始化:

c = &b;

它在内存中的大概模样大致如下:

image.png

c的类型是什么?显然它是一个指针,但它所指向的是什么?

变量b是一个“指向整型的指针”,所以任何指向b的类型必须是指向“指向整型的指针”的指针,更通俗地说,是一个指针的指针。

它合法吗?

是的!指针变量和其他变量一样,占据内存中某个特定的位置,所以用&操作符取得它的地址是合法的。

那么这个变量的声明是怎样的声明的呢?

int **c = &b;

那么这个**c如何理解呢?操作符具有从右想做的结合性,所以这个表达式相当于(*c),我们从里向外逐层求职。*c访问c所指向的位置,我们知道这是变量b.第二个间接访问操作符访问这个位置所指向的地址,也就是变量a.指针的指针并不难懂,只需要留心所有的箭头,如果表达式中出现了间接访问操作符,你就要随箭头访问它所指向的位置。

二级指针做形参输出特性

二级指针做参数的输出特性是指由被调函数分配内存。

//被调函数,由参数n确定分配多少个元素内存
void allocate_space(int **arr,int n){
 //堆上分配n个int类型元素内存
 int *temp = (int *)malloc(sizeof(int)* n);
 if (NULL == temp){
  return;
 }
 //给内存初始化值
 int *pTemp = temp;
 for (int i = 0; i < n;i ++){
  //temp[i] = i + 100;
  *pTemp = i + 100;
  pTemp++;
 }
 //指针间接赋值
 *arr = temp;
}
//打印数组
void print_array(int *arr,int n){
 for (int i = 0; i < n;i ++){
  printf("%d ",arr[i]);
 }
 printf("\n");
}
//二级指针输出特性(由被调函数分配内存)
void test(){
 int *arr = NULL;
 int n = 10;
 //给arr指针间接赋值
 allocate_space(&arr,n);
 //输出arr指向数组的内存
 print_array(arr, n);
 //释放arr所指向内存空间的值
 if (arr != NULL){
  free(arr);
  arr = NULL;
 }
}

#二级指针做形参输入特性

二级指针做形参输入特性是指由主调函数分配内存。

//打印数组
void print_array(int **arr,int n){
 for (int i = 0; i < n;i ++){
  printf("%d ",*(arr[i]));
 }
 printf("\n");
}
//二级指针输入特性(由主调函数分配内存)
void test(){
 
 int a1 = 10;
 int a2 = 20;
 int a3 = 30;
 int a4 = 40;
 int a5 = 50;

 int n = 5;

 int** arr = (int **)malloc(sizeof(int *) * n);
 arr[0] = &a1;
 arr[1] = &a2;
 arr[2] = &a3;
 arr[3] = &a4;
 arr[4] = &a5;

 print_array(arr,n);

 free(arr);
 arr = NULL;
}

#强化训练_画出内存模型图

void mian()
{
 //栈区指针数组
 char *p1[] = { "aaaaa", "bbbbb", "ccccc" };

 //堆区指针数组
 char **p3 = (char **)malloc(3 * sizeof(char *)); //char *array[3];

 int i = 0;
 for (i = 0; i < 3; i++)
 {
  p3[i] = (char *)malloc(10 * sizeof(char)); //char buf[10]
  sprintf(p3[i], "%d%d%d", i, i, i);
 }
}

#多级指针

将堆区数组指针案例改为三级指针案例:

//分配内存
void allocate_memory(char*** p, int n){

 if (n < 0){
  return;
 }

 char** temp = (char**)malloc(sizeof(char*)* n);
 if (temp == NULL){
  return;
 }

 //分别给每一个指针malloc分配内存
 for (int i = 0; i < n; i++){
  temp[i] = malloc(sizeof(char)* 30);
  sprintf(temp[i], "%2d_hello world!", i + 1);
 }

 *p = temp;
}

//打印数组
void array_print(char** arr, int len){
 for (int i = 0; i < len; i++){
  printf("%s\n", arr[i]);
 }
 printf("----------------------\n");
}

//释放内存
void free_memory(char*** buf, int len){
 if (buf == NULL){
  return;
 }

 char** temp = *buf;

 for (int i = 0; i < len; i++){
  free(temp[i]);
  temp[i] = NULL;
 }

 free(temp);
}

void test(){

 int n = 10;
 char** p = NULL;
 allocate_memory(&p, n);
 //打印数组
 array_print(p, n);
 //释放内存
 free_memory(&p, n);
}

深拷贝和浅拷贝

如果2个程序单元(例如2个函数)是通过拷贝 他们所共享的数据的 指针来工作的,这就是浅拷贝,因为真正要访问的数据并没有被拷贝。如果被访问的数据被拷贝了,在每个单元中都有自己的一份,对目标数据的操作相互 不受影响,则叫做深拷贝。

#include <iostream>
using namespace std;

class CopyDemo
{
public:
  CopyDemo(int pa,char *cstr)  //构造函数,两个参数
  {
     this->a = pa;
     this->str = new char[1024]; //指针数组,动态的用new在堆上分配存储空间
     strcpy(this->str,cstr);    //拷贝过来
  }

//没写,C++会自动帮忙写一个复制构造函数,浅拷贝只复制指针,如下注释部分
  //CopyDemo(CopyDemo& obj)  
  //{
  //   this->a = obj.a;
  //  this->str = obj.str; //这里是浅复制会出问题,要深复制
  //}

  CopyDemo(CopyDemo& obj)  //一般数据成员有指针要自己写复制构造函数,如下
  {
     this->a = obj.a;
    // this->str = obj.str; //这里是浅复制会出问题,要深复制
     this->str = new char[1024];//应该这样写
     if(str != 0)
        strcpy(this->str,obj.str); //如果成功,把内容复制过来
  }

  ~CopyDemo()  //析构函数
  {
     delete str;
  }

public:
     int a;  //定义一个整型的数据成员
     char *str; //字符串指针
};

int main()
{
  CopyDemo A(100,"hello!!!");

  CopyDemo B = A;  //复制构造函数,把A的10和hello!!!复制给B
  cout <<"A:"<< A.a << "," <<A.str << endl;
  //输出A:100,hello!!!
  cout <<"B:"<< B.a << "," <<B.str << endl;
  //输出B:100,hello!!!

  //修改后,发现A,B都被改变,原因就是浅复制,A,B指针指向同一地方,修改后都改变
  B.a = 80;
  B.str[0] = 'k';

  cout <<"A:"<< A.a << "," <<A.str << endl;
  //输出A:100,kello!!!
  cout <<"B:"<< B.a << "," <<B.str << endl;
  //输出B:80,kello!!!

  return 0;
}

位运算

可以使用 C 对变量中的个别位进行操作。您可能对人们想这样做的原因感到奇怪。这种能力有时确实是必须的,或者至少是有用的。C 提供位的逻辑运算符和移位运算符。在以下例子中,我们将使用二进制计数法写出值,以便您可以了解对位发生的操作。在一个实际程序中,您可以使用一般的形式的整数变量或常量。例如不适用 00011001 的形式,而写为 25 或者 031 或者 0x19.在我们的例子中,我们将使用8位数字,从左到右,每位的编号是 7 到 0。

位逻辑运算符

4 个位运算符用于整型数据,包括 char。将这些位运算符成为位运算的原因是它们对每位进行操作,而不影响左右两侧的位。请不要将这些运算符与常规的逻辑运算符(&& 、||和!)相混淆,常规的位的逻辑运算符对整个值进行操作。

按位取反~

一元运算符~将每个 1 变为 0,将每个 0 变为 1,如下面的例子:

~(10011010)
01100101

假设 a 是一个unsigned char,已赋值为 2。在二进制中,2 是00000010.于是 -a 的值为11111101或者 253。请注意该运算符不会改变 a 的值,a 仍为 2。

unsigned char a = 2;   //00000010
unsigned char b = ~a;  //11111101
printf("ret = %d\n", a); //ret = 2
printf("ret = %d\n", b); //ret = 253

位与(AND): &

二进制运算符 & 通过对两个操作数逐位进行比较产生一个新值。对于每个位,只有两个操作数的对应位都是 1 时结果才 为 1。

(10010011) & (00111101) = (00010001)

C 也有一个组合的位与-赋值运算符:&=。下面两个将产生相同的结果:

val &= 0377
val = val & 0377

位或(OR): |

二进制运算符 | 通过对两个操作数逐位进行比较产生一个新值。对于每个位,如果其中任意操作数中对应的位为 1,那么结果位就为 1。

(10010011)| (00111101) = (10111111)

C 也有组合位或-赋值运算符: |=

val |= 0377
val = val | 0377

**位异或: **

二进制运算符^对两个操作数逐位进行比较。对于每个位,如果操作数中的对应位有一个是 1(但不是都是1),那么结果是 1.如果都是 0 或者都是 1,则结果位 0。

(10010011)^ (00111101) = (10101110)

C 也有一个组合的位异或 - 赋值运算符: ^=

val ^= 0377
val = val ^ 0377

1用法

打开位

已知:10011010:

1.将位 2 打开

flag | 10011010

(10011010)|(00000100)=(10011110)

2.将所有位打开

flag | ~flag

(10011010)|(01100101)=(11111111)

1.1.5.2 关闭位

flag & ~flag

(10011010)&(01100101)=(00000000)

1.1.5.3 转置位

转置(toggling)一个位表示如果该位打开,则关闭该位;如果该位关闭,则打开。您可以使用位异或运算符来转置。其思想是如果 b 是一个位(1或0),那么如果 b 为 1 则 b^1 为 0,如果 b 为 0,则 1^b 为 1。无论 b 的值是 0 还是 1,0^b 为 b。

flag ^ 0xff

(10010011)^(11111111)=(01101100)

1.1.5.4 交换两个数不需要临时变量

//a ^ b = temp;
//a ^ temp = b;
//b ^ temp = a
 (10010011)^(00100110)=(10110101)
 (10110101)^(00100110)= 10010011
 
  int a = 10;
  int b = 30;

移位运算符

现在让我们了解一下 C 的移位运算符。移位运算符将位向左或向右移动。同样,我们仍将明确地使用二进制形式来说明该机制的工作原理。

左移 <<

左移运算符<<将其左侧操作数的值的每位向左移动,移动的位数由其右侧操作数指定。空出来的位用 0 填充,并且丢弃移出左侧操作数末端的位。在下面例子中,每位向左移动两个位置。

(10001010) << 2 = (00101000)

该操作将产生一个新位置,但是不改变其操作数。

1 << 1 = 2;
2 << 1 = 4;
4 << 1 = 8;
8 << 2 = 32

左移一位相当于原值 *2。

右移 >>

右移运算符>>将其左侧的操作数的值每位向右移动,移动的位数由其右侧的操作数指定。丢弃移出左侧操作数有段的位。对于unsigned类型,使用 0 填充左端空出的位。对于有符号类型,结果依赖于机器。空出的位可能用 0 填充,或者使用符号(最左端)位的副本填充。

//有符号值
(10001010) >> 2
(00100010)     //在某些系统上的结果值

(10001010) >> 2
(11100010)     //在另一些系统上的结果

//无符号值
(10001010) >> 2
(00100010)    //所有系统上的结果值

用法:移位运算符

移位运算符能够提供快捷、高效(依赖于硬件)对 2 的幂的乘法和除法。

number << n: number乘以2的n次幂

number >> n: 如果number非负,则用number除以2的n次幂

数组

一维数组

  • 元素类型角度:数组是相同类型的变量的有序集合
  • 内存角度:连续的一大片内存空间

image.png 结构体

结构体基础知识

** 结构体类型的定义**

struct Person{
 char name[64];
 int age;
};

typedef struct _PERSON{
 char name[64];
 int age;
}Person;

注意:定义结构体类型时不要直接给成员赋值,结构体只是一个类型,编译器还没有为其分配空间,只有根据其类型定义变量时,才分配空间,有空间后才能赋值。

结构体变量的定义

struct Person{
 char name[64];
 int age;
}p1; //定义类型同时定义变量

struct{
 char name[64];
 int age;
}p2; //定义类型同时定义变量

struct Person p3; //通过类型直接定义

结构体变量的初始化

struct Person{
 char name[64];
 int age;
}p1 = {"john",10}; //定义类型同时初始化变量

struct{
 char name[64];
 int age;
}p2 = {"Obama",30}; //定义类型同时初始化变量

struct Person p3 = {"Edward",33}; //通过类型直接定义

结构体成员的使用

struct Person{
 char name[64];
 int age;
};
void test(){
 //在栈上分配空间
 struct Person p1;
 strcpy(p1.name, "John");
 p1.age = 30;
 //如果是普通变量,通过点运算符操作结构体成员
 printf("Name:%s Age:%d\n", p1.name, p1.age);

 //在堆上分配空间
 struct Person* p2 = (struct Person*)malloc(sizeof(struct Person));
 strcpy(p2->name, "Obama");
 p2->age = 33;
 //如果是指针变量,通过->操作结构体成员
 printf("Name:%s Age:%d\n", p2->name, p2->age);
}

结构体赋值

赋值基本概念

相同的两个结构体变量可以相互赋值,把一个结构体变量的值拷贝给另一个结构体,这两个变量还是两个独立的变量。

struct Person{
 char name[64];
 int age;
};

void test(){
 //在栈上分配空间
 struct Person p1 = { "John" , 30};
 struct Person p2 = { "Obama", 33 };
 printf("Name:%s Age:%d\n", p1.name, p1.age);
 printf("Name:%s Age:%d\n", p2.name, p2.age);
 //将p2的值赋值给p1
 p1 = p2;
 printf("Name:%s Age:%d\n", p1.name, p1.age);
 printf("Name:%s Age:%d\n", p2.name, p2.age);
}

1.1.5.1 深拷贝和浅拷贝

//一个老师有N个学生
typedef struct _TEACHER{
 char* name;
}Teacher;


void test(){
 
 Teacher t1;
 t1.name = malloc(64);
 strcpy(t1.name , "John");

 Teacher t2;
 t2 = t1;

 //对手动开辟的内存,需要手动拷贝
 t2.name = malloc(64);
 strcpy(t2.name, t1.name);

 if (t1.name != NULL){
  free(t1.name);
  t1.name = NULL;
 }
 if (t2.name != NULL){
  free(t2.name);
  t1.name = NULL;
 }
}

结构体数组

struct Person{
 char name[64];
 int age;
};

void test(){
 //在栈上分配空间
 struct Person p1[3] = {
  { "John", 30 },
  { "Obama", 33 },
  { "Edward", 25}
 };

 struct Person p2[3] = { "John", 30, "Obama", 33, "Edward", 25 };
 for (int i = 0; i < 3;i ++){
  printf("Name:%s Age:%d\n",p1[i].name,p1[i].age);
 }
 printf("-----------------\n");
 for (int i = 0; i < 3; i++){
  printf("Name:%s Age:%d\n", p2[i].name, p2[i].age);
 }
 printf("-----------------\n");
 //在堆上分配结构体数组
 struct Person* p3 = (struct Person*)malloc(sizeof(struct Person) * 3);
 for (int i = 0; i < 3;i++){
  sprintf(p3[i].name, "Name_%d", i + 1);
  p3[i].age = 20 + i;
 }
 for (int i = 0; i < 3; i++){
  printf("Name:%s Age:%d\n", p3[i].name, p3[i].age);
 }
}

#结构体嵌套指针

结构体嵌套一级指针

struct Person{
 char* name;
 int age;
};

void allocate_memory(struct Person** person){
 if (person == NULL){
  return;
 }
 struct Person* temp = (struct Person*)malloc(sizeof(struct Person));
 if (temp == NULL){
  return;
 }
 //给name指针分配内存
 temp->name = (char*)malloc(sizeof(char)* 64);
 strcpy(temp->name, "John");
 temp->age = 100;

 *person = temp;
}

void print_person(struct Person* person){
 printf("Name:%s Age:%d\n",person->name,person->age);
}

void free_memory(struct Person** person){
 if (person == NULL){
  return;
 }
 struct Person* temp = *person;
 if (temp->name != NULL){
  free(temp->name);
  temp->name = NULL;
 }

 free(temp);
}

void test(){
 
 struct Person* p = NULL;
 allocate_memory(&p);
 print_person(p);
 free_memory(&p);
}

结构体嵌套二级指针

//一个老师有N个学生
typedef struct _TEACHER{
 char name[64];
 char** students;
}Teacher;

void create_teacher(Teacher** teacher,int n,int m){

 if (teacher == NULL){
  return;
 }

 //创建老师数组
 Teacher* teachers = (Teacher*)malloc(sizeof(Teacher)* n);
 if (teachers == NULL){
  return;
 }

 //给每一个老师分配学生
 int num = 0;
 for (int i = 0; i < n; i ++){
  sprintf(teachers[i].name, "老师_%d", i + 1);
  teachers[i].students = (char**)malloc(sizeof(char*) * m);
  for (int j = 0; j < m;j++){
   teachers[i].students[j] = malloc(64);
   sprintf(teachers[i].students[j], "学生_%d", num + 1);
   num++;
  }
 }

 *teacher = teachers; 
}

void print_teacher(Teacher* teacher,int n,int m){
 for (int i = 0; i < n; i ++){
  printf("%s:\n", teacher[i].name);
  for (int j = 0; j < m;j++){
   printf("  %s",teacher[i].students[j]);
  }
  printf("\n");
 }
}

void free_memory(Teacher** teacher,int n,int m){
 if (teacher == NULL){
  return;
 }

 Teacher* temp = *teacher;

 for (int i = 0; i < n; i ++){
  
  for (int j = 0; j < m;j ++){
   free(temp[i].students[j]);
   temp[i].students[j] = NULL;
  }

  free(temp[i].students);
  temp[i].students = NULL;
 }

 free(temp);

}

void test(){
 
 Teacher* p = NULL;
 create_teacher(&p,2,3);
 print_teacher(p, 2, 3);
 free_memory(&p,2,3);
}

结构体成员偏移量

//一旦结构体定义下来,则结构体中的成员内存布局就定下了
#include <stddef.h>
struct Teacher
{
 char a;
 int b;
};

void test01(){

 struct Teacher  t1;
 struct Teacher*p = &t1;


 int offsize1 = (int)&(p->b) - (int)p;  //成员b 相对于结构体 Teacher的偏移量
 int offsize2 = offsetof(struct Teacher, b);

 printf("offsize1:%d \n", offsize1); //打印b属性对于首地址的偏移量
 printf("offsize2:%d \n", offsize2);
}

结构体字节对齐

在用 sizeof 运算符求算某结构体所占空间时,并不是简单地将结构体中所有元素各自占的空间相加,这里涉及到内存字节对齐的问题。

从理论上讲,对于任何变量的访问都可以从任何地址开始访问,但是事实上不是如此,实际上访问特定类型的变量只能在特定的地址访问,这就需要各个变量在空间上按一定的规则排列, 而不是简单地顺序排列,这就是内存对齐

内存对齐原因

我们知道内存的最小单元是一个字节,当 cpu 从内存中读取数据的时候,是一个一个字节读取,所以内存对我们应该是入下图这样:

image.png

如何内存对齐

  • 对于标准数据类型,它的地址只要是它的长度的整数倍。
  • 对于非标准数据类型,比如结构体,要遵循一下对齐原则:
  1. 数组成员对齐规则。第一个数组成员应该放在offset为0的地方,以后每个数组成员应该放在offset为min(当前成员的大小,#pargama pack(n))整数倍的地方开始(比如int在32位机器为4字节,#pargama pack(2),那么从2的倍数地方开始存储)。
  2. 结构体总的大小,也就是sizeof的结果,必须是min(结构体内部最大成员,#pargama pack(n))的整数倍,不足要补齐。
  3. 结构体做为成员的对齐规则。如果一个结构体B里嵌套另一个结构体A,还是以最大成员类型的大小对齐,但是结构体A的起点为A内部最大成员的整数倍的地方。(struct B里存有struct A,A里有char,int,double等成员,那A应该从8的整数倍开始存储。),结构体A中的成员的对齐规则仍满足原则1、原则2。

手动设置对齐模数:

  • #pragma pack(show)

显示当前packing alignment的字节数,以warning message的形式被显示。

  • #pragma pack(push)

将当前指定的packing alignment数组进行压栈操作,这里的栈是the internal compiler stack,同事设置当前的packing alignment为n;如果n没有指定,则将当前的packing alignment数组压栈。

  • #pragma pack(pop)

从internal compiler stack中删除最顶端的reaord; 如果没有指定n,则当前栈顶record即为新的packing alignement数值;如果指定了n,则n成为新的packing alignment值

  • #pragma pack(n)

指定packing的数值,以字节为单位,缺省数值是8,合法的数值分别是1,2,4,8,16。

内存对齐案例

#pragma pack(4)

typedef struct _STUDENT{
 int a;
 char b;
 double c;
 float d;
}Student;

typedef struct _STUDENT2{
 char a;
 Student b; 
 double c;
}Student2;

void test01(){

 //Student
 //a从偏移量0位置开始存储
 //b从4位置开始存储
 //c从8位置开始存储
 //d从12位置开存储
 //所以Student内部对齐之后的大小为20 ,整体对齐,整体为最大类型的整数倍 也就是8的整数倍 为24

 printf("sizeof Student:%d\n",sizeof(Student));

 //Student2 
 //a从偏移量为0位置开始 
 //b从偏移量为Student内部最大成员整数倍开始,也就是8开始
 //c从8的整数倍地方开始,也就是32开始
 //所以结构体Sutdnet2内部对齐之后的大小为:40 , 由于结构体中最大成员为8,必须为8的整数倍 所以大小为40
 printf("sizeof Student2:%d\n", sizeof(Student2));
}

#文件操作

文件在今天的计算机系统中作用是很重要的。文件用来存放程序、文档、数据、表格、图片和其他很多种类的信息。作为一名程序员,您必须编程来创建、写入和读取文件。编写程序从文件读取信息或者将结果写入文件是一种经常性的需求。C提供了强大的和文件进行通信的方法。使用这种方法我们可以在程序中打开文件,然后使用专门的 I/O 函数读取文件或者写入文件。

文件相关概念

文件的概念

一个文件通常就是磁盘上一段命名的存储区。但是对于操作系统来说,文件就会更复杂一些。例如,一个大文件可以存储在一些分散的区段中,或者还会包含一些操作系统可以确定其文件类型的附加数据,但是这些是操作系统,而不是我们程序员所要关心的事情。我们应该考虑如何在 C 程序中处理文件。

流的概念

流是一个动态的概念,可以将一个字节形象地比喻成一滴水,字节在设备、文件和程序之间的传输就是流,类似于水在管道中的传输,可以看出,流是对输入输出源的一种抽象,也是对传输信息的一种抽象。

C语言中,I/O 操作可以简单地看作是从程序移进或移出字节,这种搬运的过程便称为流(stream)。程序只需要关心是否正确地输出了字节数据,以及是否正确地输入了要读取字节数据,特定 I/O 设备的细节对程序员是隐藏的。

文本流

文本流,也就是我们常说的以文本模式读取文件。文本流的有些特性在不同的系统中可能不同。其中之一就是文本行的最大长度。标准规定至少允许 254 个字符。另一个可能不同的特性是文本行的结束方式。例如在 Windows 系统中,文本文件约定以一个回车符和一个换行符结尾。但是在 Linux 下只使用一个换行符结尾。

标准 C 把文本定义为零个或者多个字符,后面跟一个表示结束的换行符(\n).对于那些文本行的外在表现形式与这个定义不同的系统上,库函数负责外部形式和内部形式之间的翻译。例如,在 Windows 系统中,在输出时,文本的换行符被写成一对回车/换行符。在输入时,文本中的回车符被丢弃。这种不必考虑文本的外部形势而操纵文本的能力简化了可移植程序的创建。

二进制流

二进制流中的字节将完全根据程序编写它们的形式写入到文件中,而且完全根据它们从文件或设备读取的形式读入到程序中。它们并未做任何改变。这种类型的流适用于非文本数据,但是如果你不希望I/O函数修改文本文件的行末字符,也可以把它们用于文本文件。

c语言在处理这两种文件的时候并不区分,都看成是字符流,按字节进行处理。

我们程序中,经常看到的文本方式打开文件和二进制方式打开文件仅仅体现在换行符的处理上。

比如说,在 widows 下,文件的换行符是 \r\n,而在 Linux 下换行符则是 \n。

当对文件使用文本方式打开的时候,读写的 windows 文件中的换行符 \r\n 会被替换成 \n 读到内存中,当在 windows 下写入文件的时候,\n 被替换成 \r\n 再写入文件。如果使用二进制方式打开文件,则不进行 \r\n 和 \n 之间的转换。 那么由于 Linux 下的换行符就是 \n,所以文本文件方式和二进制方式无区别。

文件的操作

文件流总览

标准库函数是的我们在 C 程序中执行与文件相关的 I/O 任务非常方便。下面是关于文件 I/O 的一般概况。

  1. 程序为同时处于活动状态的每个文件声明一个指针变量,其类型为 FILE*。这个指针指向这个 FILE 结构,当它处于活动状态时由流使用。
  2. 流通过 fopen 函数打开。为了打开一个流,我们必须指定需要访问的文件或设备以及他们的访问方式(读、写、或者读写)。Fopen 和操作系统验证文件或者设备是否存在并初始化 FILE。
  3. 根据需要对文件进行读写操作。
  4. 最后调用 fclose 函数关闭流。关闭一个流可以防止与它相关的文件被再次访问,保证任何存储于缓冲区中的数据被正确写入到文件中,并且释放 FILE 结构。

标准 I/O 更为简单,因为它们并不需要打开或者关闭。

I/O 函数以三种基本的形式处理数据:单个字符、文本行和二进制数据。对于每种形式都有一组特定的函数对它们进行处理。

输入/输出函数家族:

image.png

image.png

文件指针

我们知道,文件是由操作系统管理的单元。当我们想操作一个文件的时候,让操作系统帮我们打开文件,操作系统把我们指定要打开文件的信息保存起来,并且返回给我们一个指针指向文件的信息。文件指针也可以理解为代指打开的文件。这个指针的类型为 FILE 类型。该类型定义在 stdio.h 头文件中。通过文件指针,我们就可以对文件进行各种操作。

对于每一个 ANSI C 程序,运行时系统必须提供至少三个流-标准输入(stdin)、标准输出(stdout)、标准错误(stderr),它们都是一个指向 FILE 结构的指针。标准输入是缺省情况下的输入来源,标准输出时缺省情况下的输出设置。具体缺省值因编译器而异,通常标准输入为键盘设备、标准输出为终端或者屏幕。

image.png

ANSI C 并未规定 FILE 的成员,不同编译器可能有不同的定义。VS 下 FILE 信息如下:

struct _iobuf { 
        char  *_ptr;         //文件输入的下一个位置 
        int   _cnt;          //剩余多少字符未被读取
        char  *_base;        //指基础位置(应该是文件的其始位置) 
        int   _flag;         //文件标志 
        int   _file;         //文件的有效性验证 
        int   _charbuf;      //检查缓冲区状况,如果无缓冲区则不读取 
        int   _bufsiz;       //文件的大小 
        char  *_tmpfname;    //临时文件名 
}; 
typedef struct _iobuf FILE;

文件缓冲区

  • 文件缓冲区

ANSI C 标准采用“缓冲文件系统”处理数据文件 所谓缓冲文件系统是指系统自动地在内存区为程序中每一个正在使用的文件开辟一个文件缓冲区从内存向磁盘输出数据必须先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘去 如果从磁盘向计算机读入数据,则一次从磁盘文件将一批数据输入到内存缓冲区(充满缓冲 区),然后再从缓冲区逐个地将数据送到程序数据区(给程序变量) 。

那么文件缓冲区有什么作用呢?

如我们从磁盘里取信息,我们先把读出的数据放在缓冲区,计算机再直接从缓冲区中取数据,等缓冲区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数,再加上计算机对缓冲区的操作大大快于对磁盘的操作,故应用缓冲区可大大提高计算机的运行速度。

image.png

文件打开关闭

文件打开(fopen)

文件的打开操作表示将给用户指定的文件在内存分配一个 FILE 结构区,并将该结构的指针返回给用户程序,以后用户程序就可用此 FILE 指针来实现对指定文件的存取操作了。当使用打开函数时,必须给出文件名、文件操作方式(读、写或读写)。

FILE * fopen(const char * filename, const char * mode);

功能:打开文件

参数:

filename:需要打开的文件名,根据需要加上路径 mode:打开文件的权限设置

返回值:

成功:文件指针 失败:NULL

方式 含义 “r” 打开,只读,文件必须已经存在。 “w” 只写,如果文件不存在则创建,如果文件已存在则把文件长度截断(Truncate)为0字节。再重新写,也就是替换掉原来的文件内容文件指针指到头。 “a” 只能在文件末尾追加数据,如果文件不存在则创建 “rb” 打开一个二进制文件,只读 “wb” 打开一个二进制文件,只写 “ab" 打开一个二进制文件,追加 “r+” 允许读和写,文件必须已存在 “w+” 允许读和写,如果文件不存在则创建,如果文件已存在则把文件长度截断为0字节再重新写 。 “a+” 允许读和追加数据,如果文件不存在则创建 “rb+” 以读/写方式打开一个二进制文件 “wb+” 以读/写方式建立一个新的二进制文件 “ab+” 以读/写方式打开一个二进制文件进行追加

示例代码:

void test(){
 
 FILE *fp = NULL;

 // "\"这样的路径形式,只能在windows使用
 // "/"这样的路径形式,windows和linux平台下都可用,建议使用这种
 // 路径可以是相对路径,也可是绝对路径
 fp = fopen("../test", "w");
 //fp = fopen("..\test", "w");

 if (fp == NULL) //返回空,说明打开失败
 {
  //perror()是标准出错打印函数,能打印调用库函数出错原因
  perror("open");
  return -1;
 }
}

应该检查fopen的返回值!如何函数失败,它会返回一个NULL值。如果程序不检查错误,这个NULL指针就会传给后续的I/O函数。它们将对这个指针执行间接访问,并将失败。

文件关闭(fclose)

文件操作完成后,如果程序没有结束,必须要用 fclose() 函数进行关闭,这是因为对打开的文件进行写入时,若文件缓冲区的空间未被写入的内容填满,这些内容不会写到打开的文件中。只有对打开的文件进行关闭操作时,停留在文件缓冲区的内容才能写到该文件中去,从而使文件完整。再者一旦关闭了文件,该文件对应的FILE结构将被释放,从而使关闭的文件得到保护,因为这时对该文件的存取操作将不会进行。文件的关闭也意味着释放了该文件的缓冲区。

int fclose(FILE * stream);

功能:关闭先前 fopen() 打开的文件。此动作让缓冲区的数据写入文件中,并释放系统所提供的文件资源。

参数:stream:文件指针

返回值

成功:0 失败:-1

它表示该函数将关闭FILE指针对应的文件,并返回一个整数值。若成功地关闭了文件,则返回一个 0 值,否则返回一个非 0 值。

文件读写函数回顾

  • 按照字符读写文件:fgetc(), fputc()
  • 按照行读写文件:fputs(), fgets()
  • 按照块读写文件:fread(), fwirte()
  • 按照格式化读写文件:fprintf(), fscanf()
  • 按照随机位置读写文件:fseek(), ftell(), rewind()

字符读写函数

int fputc(int ch, FILE * stream);

功能:将 ch 转换为 unsigned char 后写入 stream 指定的文件中。

参数: ch:需要写入文件的字符。 stream:文件指针

返回值: 成功:成功写入文件的字符。 失败:返回-1

int fgetc(FILE * stream);

功能:从 stream 指定的文件中读取一个字符。

参数: stream:文件指针

返回值: 成功:返回读取到的字符。 失败:-1

int feof(FILE * stream);

功能:检测是否读取到了文件结尾。

参数: stream:文件指针

返回值: 非0值:已经到文件结尾。 0:没有到文件结尾

void test(){

 //写文件
 FILE* fp_write= NULL;
 //写方式打开文件
 fp_write = fopen("./mydata.txt", "w+");
 if (fp_write == NULL){
  return;
 }

 char buf[] = "this is a test for pfutc!";
 for (int i = 0; i < strlen(buf);i++){
  fputc(buf[i], fp_write);
 }
 
 fclose(fp_write);

 //读文件
 FILE* fp_read = NULL;
 fp_read = fopen("./mydata.txt", "r");
 if (fp_read == NULL){
  return;
 }

#if 0
 //判断文件结尾 注意:多输出一个空格
 while (!feof(fp_read)){
  printf("%c",fgetc(fp_read));
 }
#else
 char ch;
 while ((ch = fgetc(fp_read)) != EOF){
  printf("%c", ch);
 }
#endif
}

将把流指针 fp 指向的文件中的一个字符读出,并赋给 ch,当执行 fgetc() 函数时,若当时文件指针指到文件尾,即遇到文件结束标志 EOF (其对应值为 -1 ),该函数返回一个 -1 给 ch,在程序中常用检查该函数返回值是否为 -1 来判断是否已读到文件尾,从而决定是否继续。

行读写函数

int fputs(const char * str, FILE * stream);

功能:将 str 所指定的字符串写入到 stream 指定的文件中, 字符串结束符 '\0' 不写入文件。

参数

str:字符串。 stream:文件指针

返回值

成功:0。 失败:-1

char * fgets(char * str, int size, FILE * stream);

功能:从 stream 指定的文件内读入字符,保存到 str 所指定的内存空间,直到出现换行字符、读到文件结尾或是已读了 size - 1 个字符为止,最后会自动加上字符 '\0' 作为字符串结束。

参数

str:字符串。

size:指定最大读取字符串的长度(size - 1)。

stream:文件指针

void test(){

 //写文件
 FILE* fp_write= NULL;
 //写方式打开文件
 fp_write = fopen("./mydata.txt", "w+");
 if (fp_write == NULL){
  perror("fopen:");
  return;
 }

 char* buf[] = {
  "01 this is a test for pfutc!\n",
  "02 this is a test for pfutc!\n",
  "03 this is a test for pfutc!\n",
  "04 this is a test for pfutc!\n",
 };
 for (int i = 0; i < 4; i ++){
  fputs(buf[i], fp_write);
 }
 
 fclose(fp_write);

 //读文件
 FILE* fp_read = NULL;
 fp_read = fopen("./mydata.txt", "r");
 if (fp_read == NULL){
  perror("fopen:");
  return;
 }

 //判断文件结尾
 while (!feof(fp_read)){
  char temp[1024] = { 0 };
  fgets(temp, 1024, fp_read);
  printf("%s",temp);
 }

 fclose(fp_read);
}

块读写函数

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

功能:以数据块的方式给文件写入内容。

参数

ptr:准备写入文件数据的地址

size: size_t 为 unsigned int类型,此参数指定写入文件内容的块数据大小

nmemb:写入文件的块数,写入文件数据总大小为:size * nmemb

stream:已经打开的文件指针

返回值

成功:实际成功写入文件数据的块数,此值和nmemb相等

失败:0

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

功能:以数据块的方式从文件中读取内容

参数

ptr:存放读取出来数据的内存空间

size: size_t 为 unsigned int类型,此参数指定读取文件内容的块数据大小

nmemb:读取文件的块数,读取文件数据总大小为:size * nmemb

stream:已经打开的文件指针

返回值

成功:实际成功读取到内容的块数,如果此值比nmemb小,但大于0,说明读到文件的结尾。

失败:0

typedef struct _TEACHER{
 char name[64];
 int age;
}Teacher;

void test(){

 //写文件
 FILE* fp_write= NULL;
 //写方式打开文件
 fp_write = fopen("./mydata.txt", "wb");
 if (fp_write == NULL){
  perror("fopen:");
  return;
 }

 Teacher teachers[4] = {
  { "Obama", 33 },
  { "John", 28 },
  { "Edward", 45},
  { "Smith", 35}
 };
 
 for (int i = 0; i < 4; i ++){
  fwrite(&teachers[i],sizeof(Teacher),1, fp_write);
 }
 //关闭文件
 fclose(fp_write);

 //读文件
 FILE* fp_read = NULL;
 fp_read = fopen("./mydata.txt", "rb");
 if (fp_read == NULL){
  perror("fopen:");
  return;
 }

 Teacher temps[4];
 fread(&temps, sizeof(Teacher), 4, fp_read);
 for (int i = 0; i < 4;i++){
  printf("Name:%s Age:%d\n",temps[i].name,temps[i].age);
 }

 fclose(fp_read);
}

格式化读写函数

int fprintf(FILE * stream, const char * format, ...);

功能:根据参数format字符串来转换并格式化数据,然后将结果输出到stream指定的文件中,指定出现字符串结束符 '\0' 为止。 参数

stream:已经打开的文件

format:字符串格式,用法和printf()一样

返回值

成功:实际写入文件的字符个数

失败:-1

int fscanf(FILE * stream, const char * format, ...);

功能:从stream指定的文件读取字符串,并根据参数format字符串来转换并格式化数据。

参数

stream:已经打开的文件

format:字符串格式,用法和scanf()一样

返回值

成功:实际从文件中读取的字符个数

失败: - 1

注意:fscanf遇到空格和换行时结束。

void test(){

 //写文件
 FILE* fp_write= NULL;
 //写方式打开文件
 fp_write = fopen("./mydata.txt", "w");
 if (fp_write == NULL){
  perror("fopen:");
  return;
 }

 fprintf(fp_write,"hello world:%d!",10);

 //关闭文件
 fclose(fp_write);

 //读文件
 FILE* fp_read = NULL;
 fp_read = fopen("./mydata.txt", "rb");
 if (fp_read == NULL){
  perror("fopen:");
  return;
 }
 
 char temps[1024] = { 0 };
 while (!feof(fp_read)){
  fscanf(fp_read, "%s", temps);
  printf("%s", temps);
 }

 fclose(fp_read);
}

随机读写函数

int fseek(FILE *stream, long offset, int whence);

功能:移动文件流(文件光标)的读写位置。

参数

stream:已经打开的文件指针

offset:根据 whence 来移动的位移数(偏移量),可以是正数,也可以负数,如果正数,则相对于 whence 往右移动,如果是负数,则相对于 whence 往左移动。如果向前移动的字节数超过了文件开头则出错返回,如果向后移动的字节数超过了 文件末尾,再次写入时将增大文件尺寸。

whence:其取值如下:

  • SEEK_SET:从文件开头移动offset个字节
  • SEEK_CUR:从当前位置移动offset个字节
  • SEEK_END:从文件末尾移动offset个字节

返回值

成功:0

失败:-1

long ftell(FILE *stream);

功能:获取文件流(文件光标)的读写位置。

参数:stream:已经打开的文件指针

返回值

成功:当前文件流(文件光标)的读写位置

失败:-1

void rewind(FILE *stream);

功能:把文件流(文件光标)的读写位置移动到文件开头。

参数:stream:已经打开的文件指针

返回值:无返回值

typedef struct _TEACHER{
 char name[64];
 int age;
}Teacher;

void test(){
 //写文件
 FILE* fp_write = NULL;
 //写方式打开文件
 fp_write = fopen("./mydata.txt", "wb");
 if (fp_write == NULL){
  perror("fopen:");
  return;
 }

 Teacher teachers[4] = {
  { "Obama", 33 },
  { "John", 28 },
  { "Edward", 45 },
  { "Smith", 35 }
 };

 for (int i = 0; i < 4; i++){
  fwrite(&teachers[i], sizeof(Teacher), 1, fp_write);
 }
 //关闭文件
 fclose(fp_write);

 //读文件
 FILE* fp_read = NULL;
 fp_read = fopen("./mydata.txt", "rb");
 if (fp_read == NULL){
  perror("fopen:");
  return;
 }

 Teacher temp;
 //读取第三个数组
 fseek(fp_read , sizeof(Teacher) * 2 , SEEK_SET);
 fread(&temp, sizeof(Teacher), 1, fp_read);
 printf("Name:%s Age:%d\n",temp.name,temp.age);

 memset(&temp,0,sizeof(Teacher));

 fseek(fp_read, -(int)sizeof(Teacher), SEEK_END);
 fread(&temp, sizeof(Teacher), 1, fp_read);
 printf("Name:%s Age:%d\n", temp.name, temp.age);

 rewind(fp_read);
 fread(&temp, sizeof(Teacher), 1, fp_read);
 printf("Name:%s Age:%d\n", temp.name, temp.age);

 fclose(fp_read);
}

文件读写案例

读写配置文件

配置文件格式如下: 正式的数据以 ‘:’ 冒号进行分割,冒号前为 key 起到索引作用,冒号后为 value 是实值。# 开头的为注释,而不是正式数据

#英雄的Id heroId:1 #英雄的姓名 heroName:德玛西亚 #英雄的攻击力 heroAtk:1000 #英雄的防御力 heroDef:500 #英雄的简介 heroInfo:前排坦克

struct ConfigInfo
{
 char key[64];
 char value[64];
};

//获取文件有效行数
int getFileLine(const char  * filePath)
{
 FILE * file = fopen(filePath, "r");
 char buf[1024] = {0};
 int lines = 0;
 while (fgets(buf,1024,file) != NULL)
 {
  if (isValidLine(buf))
  {
   lines++;
  }
  memset(buf, 0, 1024);
 }
  
 fclose(file);
 
 return lines;

}
//解析文件
void parseFile(const char  * filePath, int lines, struct ConfigInfo** configInfo)
{

 struct ConfigInfo * pConfig =  malloc(sizeof(struct ConfigInfo) * lines);

 if (pConfig == NULL)
 {
  return;
 }

 FILE * file = fopen(filePath, "r");
 char buf[1024] = { 0 };
 
 int index = 0;
 while (fgets(buf, 1024, file) != NULL)
 {
  if (isValidLine(buf))
  {
   //解析数据到struct ConfigInfo中
   memset(pConfig[index].key, 0, 64);
   memset(pConfig[index].value, 0, 64);
   char * pos = strchr(buf, ':');
   strncpy(pConfig[index].key, buf, pos - buf);
   strncpy(pConfig[index].value, pos + 1, strlen(pos + 1) - 1); // 从第二个单词开始截取字符串,并且不截取换行符
   //printf("key = %s\n", pConfig[index].key);
   //printf("value = %s\n", pConfig[index].value);
   index++;
  }
  memset(buf, 0, 1024);
 }

 *configInfo = pConfig;

}

//获取指定的配置信息
char * getInfoByKey(char * key, struct ConfigInfo*configInfo ,int lines)
{
 for (int i = 0; i < lines;i++)
 {
  if (strcmp(key, configInfo[i].key) == 0)
  {
   return configInfo[i].value;
  }
 }
 return NULL;
}

//释放配置文件信息
void freeConfigInfo(struct ConfigInfo*configInfo)
{
 free(configInfo);
 configInfo = NULL;
}

//判断当前行是否为有效行
int isValidLine(char * buf)
{
 if (buf[0] == '0' || buf[0] == '\0' || strchr(buf,':') == NULL)
 {
  return 0;// 如果行无限 返回假
 }
 return 1;
}

int main(){

 char * filePath = "./config.txt";
 int lines = getFileLine(filePath);
 printf("文件有效行数为:%d\n", lines);

 struct ConfigInfo * config = NULL;
 parseFile(filePath, lines, &config);

 printf("heroId = %s\n", getInfoByKey("heroId", config, lines));
 printf("heroName: = %s\n", getInfoByKey("heroName", config, lines));
 printf("heroAtk = %s\n", getInfoByKey("heroAtk", config, lines));
 printf("heroDef: = %s\n", getInfoByKey("heroDef", config, lines));
 printf("heroInfo: = %s\n", getInfoByKey("heroInfo", config, lines));

 freeConfigInfo(config);
 config = NULL;

 system("pause");
 return EXIT_SUCCESS;
}