C 语言高级1-内存分区,多级指针,位运算

目录

[1. 内存分区](#1. 内存分区)

[1.1 数据类型](#1.1 数据类型)

[1.1.1 数据类型概念](#1.1.1 数据类型概念)

[1.1.2 数据类型别名](#1.1.2 数据类型别名)

[1.1.3 void数据类型](#1.1.3 void数据类型)

[1.1.4 sizeof操作符](#1.1.4 sizeof操作符)

[1.1.5 数据类型总结](#1.1.5 数据类型总结)

[1.2 变量](#1.2 变量)

[1.1.1 变量的概念](#1.1.1 变量的概念)

[3.1.2 变量名的本质](#3.1.2 变量名的本质)

[1.3 程序的内存分区模型](#1.3 程序的内存分区模型)

[1.3.1 内存分区](#1.3.1 内存分区)

[1.3.1.1 运行之前](#1.3.1.1 运行之前)

1.3.1.2运行之后

[1.3.2 分区模型](#1.3.2 分区模型)

[1.3.2.1 栈区](#1.3.2.1 栈区)

[1.3.2.2 堆区](#1.3.2.2 堆区)

[1.3.2.3 全局/静态区](#1.3.2.3 全局/静态区)

[1.3.2.4 总结](#1.3.2.4 总结)

[1.3.3 函数调用模型](#1.3.3 函数调用模型)

[1.3.3.1 函数调用流程](#1.3.3.1 函数调用流程)

[1.3.3.2 调用惯例](#1.3.3.2 调用惯例)

[1.3.3.3 函数变量传递分析](#1.3.3.3 函数变量传递分析)

[1.3.4 栈的生长方向和内存存放方向](#1.3.4 栈的生长方向和内存存放方向)

2指针强化

[2.1 指针是一种数据类型](#2.1 指针是一种数据类型)

[2.1.1 指针变量](#2.1.1 指针变量)

[2.1.2 野指针和空指针](#2.1.2 野指针和空指针)

[2.1.2.1 空指针](#2.1.2.1 空指针)

[2.1.2.2 野指针](#2.1.2.2 野指针)

[2.1.3 间接访问操作符](#2.1.3 间接访问操作符)

[2.1.4 指针的步长](#2.1.4 指针的步长)

[2.2 指针的意义_间接赋值](#2.2 指针的意义_间接赋值)

[2.2.1 间接赋值的三大条件](#2.2.1 间接赋值的三大条件)

[2.2.2 如何定义合适的指针变量](#2.2.2 如何定义合适的指针变量)

[2.2.3 间接赋值:从0级指针到1级指针](#2.2.3 间接赋值:从0级指针到1级指针)

[2.2.4 间接赋值:从1级指针到2级指针](#2.2.4 间接赋值:从1级指针到2级指针)

[2.2.5 间接赋值的推论](#2.2.5 间接赋值的推论)

[2.3 指针做函数参数](#2.3 指针做函数参数)

[2.3.1 输入特性](#2.3.1 输入特性)

[2.3.2 输出特性](#2.3.2 输出特性)

[2.4 字符串指针强化](#2.4 字符串指针强化)

[2.4.1 字符串指针做函数参数](#2.4.1 字符串指针做函数参数)

[2.4.1.2 字符串拷贝功能实现](#2.4.1.2 字符串拷贝功能实现)

[2.4.1.3 字符串反转模型](#2.4.1.3 字符串反转模型)

[2.4.2 字符串的格式化](#2.4.2 字符串的格式化)

[2.4.2.1 sprintf](#2.4.2.1 sprintf)

[2.4.2.2 sscanf](#2.4.2.2 sscanf)

[2.5 一级指针易错点](#2.5 一级指针易错点)

[2.5.1 越界](#2.5.1 越界)

[2.5.2 指针叠加会不断改变指针指向](#2.5.2 指针叠加会不断改变指针指向)

[2.5.3 返回局部变量地址](#2.5.3 返回局部变量地址)

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

[2.6 const使用](#2.6 const使用)

[3. 指针的指针(二级指针)](#3. 指针的指针(二级指针))

[3.1 二级指针基本概念](#3.1 二级指针基本概念)

[3.2 二级指针做形参输出特性](#3.2 二级指针做形参输出特性)

[3.3 二级指针做形参输入特性](#3.3 二级指针做形参输入特性)

[3.4 强化训练_画出内存模型图](#3.4 强化训练_画出内存模型图)

[3.4 多级指针](#3.4 多级指针)

4.位运算

4.1位逻辑运算符

[4.1.1 按位取反](#4.1.1 按位取反)

[4.1.2位与(AND): &](#4.1.2位与(AND): &)

4.1.4位异或:

[4.1.5.1 打开位](#4.1.5.1 打开位)

[4.1.5.2 关闭位](#4.1.5.2 关闭位)

[4.1.5.3 转置位](#4.1.5.3 转置位)

[4.1.5.4 交换两个数不需要临时变量](#4.1.5.4 交换两个数不需要临时变量)

[4.2 移位运算符](#4.2 移位运算符)

[4.2.1 左移 <<](#4.2.1 左移 <<)

[4.2.2 右移 >>](#4.2.2 右移 >>)

[4.2.3 用法:移位运算符](#4.2.3 用法:移位运算符)


1. 内存分区

1.1 数据类型

1.1.1 数据类型概念

|----------------------------------------------------|
| 什么是数据类型?为什么需要数据类型? 数据类型是为了更好进行内存的管理,让编译器能确定分配多少内存。 |

我们现实生活中,狗是狗,鸟是鸟等等,每一种事物都有自己的类型,那么程序中使用数据类型也是来源于生活。

当我们给狗分配内存的时候,也就相当于给狗建造狗窝,给鸟分配内存的时候,也就是给鸟建造一个鸟窝,我们可以给他们各自建造一个别墅,但是会造成内存的浪费,不能很好的利用内存空间。

我们在想,如果给鸟分配内存,只需要鸟窝大小的空间就够了,如果给狗分配内存,那么也只需要狗窝大小的内存,而不是给鸟和狗都分配一座别墅,造成内存的浪费。当我们定义一个变量,a = 10,编译器如何分配内存?计算机只是一个机器,它怎么知道用多少内存可以放得下10?所以说,数据类型非常重要,它可以告诉编译器分配多少内存可以放得下我们的数据。

|-----------------------------------------|
| 狗窝里面是狗,鸟窝里面是鸟,如果没有数据类型,你怎么知道冰箱里放得是一头大象! |

|-----------------------------------------------------------------------------------------------------------|
| * 类型是对数据的抽象; * 类型相同的数据具有相同的表示形式、存储格式以及相关操作; * 程序中所有的数据都必定属于某种数据类型; * 数据类型可以理解为创建变量的模具: 固定大小内存的别名; |

1**.1.2 数据类型别名**

cpp 复制代码
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;
}

1.1.3 void数据类型

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

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

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

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

//2. 不能定义void类型变量
void test02(){
	void val; //报错
}
cpp 复制代码
//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);
}

1.1.4 sizeof操作符

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

基本语法:

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

sizeof 注意点:

  • sizeof返回的占用空间大小是为这个变量开辟的大小,而不只是它用到的空间。和现今住房的建筑面积和实用面积的概念差不多。所以对结构体用的时候,大多情况下就得考虑字节对齐的问题了;
  • sizeof返回的数据结果类型是unsigned int;

要注意数组名和指针变量的区别。通常情况下,我们总觉得数组名和指针变量差不多,但是在用sizeof的时候差别很大,对数组名用sizeof返回的是整个数组的大小,而对指针变量进行操作的时候返回的则是指针变量本身所占得空间,在32位机的条件下一般都是4。而且当数组名作为函数参数时,在函数内部,形参也就是个指针,所以不再返回数组的大小.

cpp 复制代码
//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);
}

1.1.5 数据类型总结

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

1.2 变量

1.1.1 变量的概念

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

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

|--------------------------------|
| 变量定义形式: 类型 标识符, 标识符, ... , 标识符 |

3.1.2 变量名的本质

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

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

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

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

1.3 程序的内存分区模型

1.3.1 内存分区

1.3.1.1 运行之前

我们要想执行我们编写的c程序,那么第一步需要对这个程序进行编译。
*

|-----------------------------------------------------------------------------------------------------------|
| 1. 预处理:宏定义展开、头文件展开、条件编译,这里并不会检查语法 2. 编译:检查语法,将预处理后文件编译生成汇编文件 3. 汇编:将汇编文件生成目标文件(二进制文件) 4. 链接:将目标文件链接为可执行程序 |

当我们编译完成生成可执行文件之后,我们通过在linux下size命令可以查看一个可执行二进制文件基本情况:

通过上图可以得知,在没有运行程序前,也就是说程序没有加载到内存前,可执行程序内部已经分好3段信息,分别为代码区(text)、数据区(data)和未初始化数据区(bss)3 个部分(有些人直接把data和bss合起来叫做静态区或全局区)。

|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1. 代码区 存放 CPU 执行的机器指令。通常代码区是可共享 的(即另外的执行程序可以调用它),使其可共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可。代码区通常是只读 的,使其只读的原因是防止程序意外地修改了它的指t令。另外,代码区还规划了局部变量的相关信息。 1. 全局初始化数据区/静态数据区(data段) 该区包含了在程序中明确被初始化的全局变量、已经初始化的静态变量(包括全局静态变量和t)和常量数据(如字符串常量)。 1. 未初始化数据区(又叫 bss 区) 存入的是全局未初始化变量和未初始化静态变量。未初始化数据区的数据在程序开始执行之前被内核初始化为 0 或者空(NULL)。 |

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

那为什么把程序的指令和程序数据分开呢?

|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1. 程序被load到内存中之后,可以将数据和代码分别映射到两个内存区域。由于数据区域对进程来说是可读可写的,而指令区域对程序来讲说是只读的,所以分区之后呢,可以将程序指令区域和数据区域分别设置成可读可写或只读。这样可以防止程序的指令有意或者无意被修改; 2. 当系统中运行着多个同样的程序的时候,这些程序执行的指令都是一样的,所以只需要内存中保存一份程序的指令就可以了,只是每一个程序运行中数据不一样而已,这样可以节省大量的内存。比如说之前的Windows Internet Explorer 7.0运行起来之后, 它需要占用112 844KB的内存,它的私有部分数据有大概15 944KB,也就是说有96 900KB空间是共享的,如果程序中运行了几百个这样的进程,可以想象共享的方法可以节省大量的内存。 |

1.3.1.2运行之后

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

|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1. 代码区(text segment) 加载的是可执行文件代码段,所有的可执行代码都加载到代码区,这块内存是不可以在运行期间修改的。 1. 未初始化数据区(BSS) 加载的是可执行文件BSS段,位置可以分开亦可以紧靠数据段,存储于数据段的数据(全局未初始化,静态未初始化数据)的生存周期为整个程序运行过程。 1. 全局初始化数据区/静态数据区(data segment) 加载的是可执行文件数据段,存储于数据段(全局初始化,静态初始化数据,文字常量(只读))的数据的生存周期为整个程序运行过程。 1. 栈区(stack) 栈是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数值、返回值、局部变量等。在程序运行过程中实时加载和释放,因此,局部变量的生存周期为申请到释放该段栈空间。 1. 堆区(heap) 堆是一个大容器,它的容量要远远大于栈,但没有栈那样先进后出的顺序。用于动态内存分配。堆在内存中位于BSS区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。 |

|------------|---------|----------|---------------------|
| 类型 | 作用域 | 生命周期 | 存储位置 |
| auto变量 | 一对{}内 | 当前函数 | 栈区 |
| static局部变量 | 一对{}内 | 整个程序运行期 | 初始化在data段,未初始化在BSS段 |
| extern变量 | 整个程序 | 整个程序运行期 | 初始化在data段,未初始化在BSS段 |
| static全局变量 | 当前文件 | 整个程序运行期 | 初始化在data段,未初始化在BSS段 |
| extern函数 | 整个程序 | 整个程序运行期 | 代码区 |
| static函数 | 当前文件 | 整个程序运行期 | 代码区 |
| register变量 | 一对{}内 | 当前函数 | 运行时存储在CPU寄存器 |
| 字符串常量 | 当前文件 | 整个程序运行期 | data段 |

|-----------------------------------|
| 注意:建立正确程序运行内存布局图是学好C的关键!! |

1.3.2 分区模型

1.3.2.1 栈区

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

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

1**.3.2.2 堆区**

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

cpp 复制代码
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);
}

|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| #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 |

calloc 和 realloc

calloc 和malloc 都是在堆区分配内存

与malloc不同的是,calloc会将空间初始化为0

calloc(个数,大小)

realloc 重新分配内存

  1. 如果重新分配的内存比原来大,那么不会初始化新空间为0
  2. 先看后续空间,如果足够,那么直接扩展
  3. 如果后续空闲空间不足,那么申请足够大的空间,将原有数据拷贝到新空间下,释放掉原有空间,将新空间的首地址返回
  4. 如果重新分配的内存比原来小,那么释放后序空间,只有权限操作申请空间

示例代码:

cpp 复制代码
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.3.2.3 全局/静态区

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

注意

|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| (1)这里不区分初始化和未初始化的数据区,是因为静态存储区内的变量若不显示初始化,则编译器会自动以默认的方式进行初始化,即静态存储区内不存在未初始化的变量。 (2)全局静态存储区内的常量分为常变量和字符串常量,一经初始化,不可修改。静态存储内的常变量是全局变量,与局部常变量不同,区别在于局部常变量存放于栈,实际可间接通过指针或者引用进行修改,而全局常变量存放于静态常量区则不可以间接修改。 (3)字符串常量存储在全局/静态存储区的常量区。 |

示例代码

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

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

void test(){
	static int v4 = 20; //全局/静态区
}

加深理解

cpp 复制代码
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);
}

字符串常量 是否可修改?字符串常量优化:

|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ANSI C中规定:修改字符串常量,结果是未定义的。 ANSI C并没有规定编译器的实现者对字符串的处理,例如: 1.有些编译器可修改字符串常量,有些编译器则不可修改字符串常量。 2.有些编译器把多个相同的字符串常量看成一个(这种优化可能出现在字符串常量中,节省空间),有些则不进行此优化。如果进行优化,则可能导致修改一个字符串常量导致另外的字符串常量也发生变化,结果不可知。 所以尽量不要去修改字符串常量 |
| C99标准: char *p = "abc"; defines p with type ''pointer to char'' and initializes it to point to an object with type ''array of char'' with length 4 whose elements are initialized with a character string literal. If an attempt is made to use p to modify the contents of the array, the behavior is undefined . |

字符串常量 是否可修改?字符串常量优化:

|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 字符串常量地址是否相同 tc2.0 ,同文件 字符串常量地址不同 V s 2013,字符串常量地址同文件和不同文件都相同 D e v c++ 、QT 同文件相同 不同文件不同 |

1.3.2.4 总结

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

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

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

1.3.3 函数调用模型

1.3.3.1 函数调用流程

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

在经典的计算机科学中,栈被定义为一个特殊的容器,用户可以将数据压入栈中(入栈,push),也可以将压入栈中的数据弹出(出栈,pop),但是栈容器必须遵循一条规则:先入栈的数据最后出栈(First In Last Out,FILO).在经典的操作系统中,栈总是向下增长的。压栈的操作使得栈顶的地址减小,弹出操作使得栈顶地址增大。栈在程序运行中具有极其重要的地位。最重要的,栈保存一个函数调用所需要维护的信息,这通常被称为堆栈帧(Stack Frame)或者活动记录(Activate Record).一个函数调用过程所需要的信息一般包括以下几个方面。

  1. 函数的返回地址;
  2. 函数的参数;
  3. 临时变量;
  4. 保存的上下文:包括在函数调用前后需要保持不变的寄存器。
cpp 复制代码
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;
}

1**.3.3.2** 调用惯例

|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 现在,我们大致了解了函数调用的过程,这期间有一个现象,那就是函数的调用者和被调用者对函数调用有着一致的理解,例如,它们双方都一致的认为函数的参数是按照某个固定的方式压入栈中。如果不这样的话,函数将无法正确运行。 如果函数调用方在传递参数的时候先压入a参数,再压入b参数,而被调用函数则认为先压入的是b,后压入的是a,那么被调用函数在使用a,b值时候,就会颠倒。 因此,函数的调用方和被调用方对于函数是如何调用的必须有一个明确的约定,只有双方都遵循同样的约定,函数才能够被正确的调用,这样的约定被称为 " 调用惯例(Calling Convention)".一个调用惯例一般包含以下几个方面: 函数参数的传递顺序和方式 函数的传递有很多种方式,最常见的是通过栈传递。函数的调用方将参数压入栈中,函数自己再从栈中将参数取出。对于有多个参数的函数,调用惯例要规定函数调用方将参数压栈的顺序:从左向右,还是从右向左。有些调用惯例还允许使用寄存器传递参数,以提高性能。 栈的维护方式 在函数将参数压入栈中之后,函数体会被调用,此后需要将被压入栈中的参数全部弹出,以使得栈在函数调用前后保持一致。这个弹出的工作可以由函数的调用方来完成,也可以由函数本身来完成。 为了在链接的时候对调用惯例进行区分,调用惯例要对函数本身的名字进行修饰。不同的调用惯例有不同的名字修饰策略。 事实上,在c语言里,存在着多个调用惯例,而默认的是cdecl.任何一个没有显示指定调用惯例的函数都是默认是cdecl惯例。比如我们上面对于func函数的声明,它的完整写法应该是: int cdecl func(int a,int b); ****注意: cdecl不是标准的关键字,在不同的编译器里可能有不同的写法,例如gcc里就不存在_cdecl这样的关键字,而是使用__attribute((cdecl)). |

|----------|-------|-------------------------|-----------------|
| 调用惯例 | 出栈方 | 参数传递 | 名字修饰 |
| cdecl | 函数调用方 | 从右至左参数入栈 | 下划线+函数名 |
| stdcall | 函数本身 | 从右至左参数入栈 | 下划线+函数名+@+参数字节数 |
| fastcall | 函数本身 | 前两个参数由寄存器传递,其余参数通过堆栈传递。 | @+函数名+@+参数的字节数 |
| pascal | 函数本身 | 从左至右参数入栈 | 较为复杂,参见相关文档 |

1**.3.3.3 函数变量传递分析**

1**.3.4 栈的生长方向和内存存放方向**

cpp 复制代码
//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 = &num;

	//从首地址开始的第一个字节
	printf("%x\n",*p);
	printf("%x\n", *(p + 1));
	printf("%x\n", *(p + 2));
	printf("%x\n", *(p + 3));
}

2指针强化

2.1 指针是一种数据类型

2.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**);** } |

2.1.2 野指针和空指针

2.1.2.1 空指针

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

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

如果对一个NULL指针间接访问会发生什么呢?结果因编译器而异。

不允许向NULL和非法地址拷贝内存

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

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

2.1.2.2 野指针

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

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

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

|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1. 指针变量未初始化 任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。 1. 指针释放后未置空 有时指针在free或delete后未赋值 NULL,便会使人以为是合法的。别看free和delete的名字(尤其是delete),它们只是把指针所指的内存给释放掉,但并没有把指针本身干掉。此时指针指向的就是"垃圾"内存。释放后的指针应立即将指针置为NULL,防止产生"野指针"。 1. 指针操作超越变量作用域 不要返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放。 |

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

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

|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1. 初始化时置 NULL 指针变量一定要初始化为NULL,因为任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的。 1. 释放时置 NULL 当指针p指向的内存空间释放时,没有设置指针p的值为NULL。delete和free只是把内存空间释放了,但是并没有将指针p的值赋为NULL。通常判断一个指针是否合法,都是使用if语句测试该指针是否为NULL。 |

2.1.3 间接访问操作符

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

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

int arr[5];

int *p = * (&arr);

int arr1[5][3] arr1 = int(*)[3]

&arr1

|------------------------------------------------------------------------------------------------------------------------------------------------|
| 1. 在指针声明时,* 号表示所声明的变量为指针 2. 在指针使用时,* 号表示操作指针所指向的内存空间 1)* 相当通过地址(指针变量的值)找到指针指向的内存,再操作内存 2)* 放在等号的左边赋值(给内存赋值,写内存) 3)* 放在等号的右边取值(从内存中取值,读内存) |

cpp 复制代码
//解引用
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);
}

2.1.4 指针的步长

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

思考如下问题:

cpp 复制代码
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);

2.2 指针的意义_间接赋值

2.2.1 间接赋值的三大条件

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

1)2个变量(一个普通变量一个指针变量、或者一个实参一个形参)

2)建立关系

3)通过 * 操作指针指向的内存

cpp 复制代码
void test(){
	int a = 100;	//两个变量
	int *p = NULL;
	//建立关系
	//指针指向谁,就把谁的地址赋值给指针
	p = &a;
	//通过*操作内存
	*p = 22;
}

2.2.2 如何定义合适的指针变量

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

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

cpp 复制代码
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);
}

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

cpp 复制代码
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");
	}
}

2.2.5 间接赋值的推论

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

2.3 指针做函数参数

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

输入:主调函数分配内存

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

2.3.1 输入特性

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

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

2.3.2 输出特性

cpp 复制代码
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);
	}

2.4 字符串指针强化

2.4.1 字符串指针做函数参数

2.4.1.1 字符串基本操作

cpp 复制代码
//字符串基本操作
//字符串是以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码对应的数值。 比如 : 1. '\063'表示的是字符'3',因为'3'的ASCII码是30(十六进制),48(十进制),63(八进制)。 2. '\x41'表示的是字符'A',因为'A'的ASCII码是41(十六进制),65(十进制),101(八进制)。 |

2.4.1.2 字符串拷贝功能实现

cpp 复制代码
//拷贝方法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++){}
}

2.4.1.3 字符串反转模型

cpp 复制代码
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);
}

2.4.2 字符串的格式化

2.4.2.1 s printf

|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| #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**);** } |

2**.4.2.2** sscanf

|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| #include <stdio.h> int sscanf**(** const char * str**,** const char * format**,** ...); 功能: 从str指定的字符串读取数据,并根据参数format字符串来转换并格式化数据。 参数: str:指定的字符串首地址 format:字符串格式,用法和scanf**()** 一样 返回值: 成功:成功则返回参数数目,失败则返回-1 失败: - 1 |

|-------------|---------------------|
| 格式 | 作用 |
| %*s或%*d | 跳过数据 |
| %[width]s | 读指定宽度的数据 |
| %[a-z] | 匹配a到z中任意字符(尽可能多的匹配) |
| %[aBc] | 匹配a、B、c中一员,贪婪性 |
| %[^a] | 匹配非a的任意字符,贪婪性 |
| %[^a-z] | 表示读取除a-z以外的所有字符 |

cpp 复制代码
//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);
}

课堂小练习

|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1. 已给定字符串为: helloworld@itcast.cn,请编码实现helloworld输出和itcast.cn输出。 2. 已给定字符串为:123abcd$myname@000qwe.请编码实现匹配出myname字符串,并输出. |

2.5 一级指针易错点

2.5.1 越界

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

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

cpp 复制代码
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);
}

2.5.3 返回局部变量地址

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

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

cpp 复制代码
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);
	}
}

2.6 const使用

cpp 复制代码
//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);
}

3. 指针的指针(二级指针)

3.1 二级指针基本概念

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

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

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

|-----------------------------------|
| c = & b****;**** |

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

问题是:c的类型是什么?显然它是一个指针,但它所指向的是什么?变量b是一个"指向整型的指针",所以任何指向b的类型必须是指向"指向整型的指针"的指针,更通俗地说,是一个指针的指针。

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

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

|----------------------------------------------------|
| int ** c = & b****;**** |

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

3.2 二级指针做形参输出特性

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

cpp 复制代码
//被调函数,由参数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;
	}
}

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

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

cpp 复制代码
//打印数组
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;
}

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

cpp 复制代码
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);
	}
}

3.4 多级指针

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

cpp 复制代码
//分配内存
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);
}

4.位运算

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

4.1位逻辑运算符

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

4.1.1 按位取反

一元运算符~将每个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 |

4.1.2 位与( AND : &

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

|----------------------------------------------------------------------------------------------------------|
| ( 10010011****)**** & ( 00111101****)**** = ( 00010001****)**** |

|--------------------------------------------------------|
| val &= 0377 val = val & 0377 |

4.1.3位或( OR : |

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

|----------------------------------------------------------------------------------------------------------|
| ( 10010011****)**** | ( 00111101****)**** = ( 10111111****)**** |

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

|--------------------------------------------------------|
| val |= 0377 val = val | 0377 |

4.1.4 位异或 :

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

|----------------------------------------------------------------------------------------------------------|
| ( 10010011****)**** ^ ( 00111101****)**** = ( 10101110****)**** |

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

|--------------------------------------------------------|
| val ^= 0377 val = val ^ 0377 |

4.1.5用法

4.1.5.1 打开位

已知:10011010:

  1. 将位2打开

flag | 10011010

|----------------------------------------------------------------------------------------|
| ( 10011010****)**** |( 00000100****)**** =( 10011110****)**** |

  1. 将所有位打开。

flag | ~flag

|----------------------------------------------------------------------------------------|
| ( 10011010****)**** |( 01100101****)**** =( 11111111****)**** |

4.1.5.2 关闭位

flag & ~flag

|----------------------------------------------------------------------------------------|
| ( 10011010****)**** &( 01100101****)**** =( 00000000****)**** |

4.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****)**** |

4.1.5. 4 交换两个数不需要临时变量

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

4.2 移位运算符

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

4.2.1 左移 <<

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

|------------------------------------------------------------------------|
| ( 10001010****)**** << 2 ( 00101000****)**** |

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

|-------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 << 1 = 2****;**** 2 << 1 = 4****;**** 4 << 1 = 8****;**** 8 << 2 = 32 |

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

4.2. 2 右移 >>

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

|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| //有符号值 ( 10001010****)**** >> 2 ( 00100010****)**** //在某些系统上的结果值 ( 10001010****)**** >> 2 ( 11100010****)**** //在另一些系统上的结果 //无符号值 ( 10001010****)**** >> 2 ( 00100010****)**** //所有系统上的结果值 |

4.2. 3 用法:移位运算符

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

|---------------|----------------------------|
| number << n | number乘以2的n次幂 |
| number >> n | 如果number非负,则用number除以2的n次幂 |

相关推荐
c沫栀42 分钟前
C - Sierpinski carpet(abc357)
c语言·c++·算法
Miracle_86.1 小时前
【数据结构】单链表:数据结构中的舞者,穿梭于理论与实践的舞池
c语言·数据结构·链表·学习方法
DdddJMs__1351 小时前
C语言 | Leetcode C语言题解之第220题存在重复元素III
c语言·leetcode·题解
DdddJMs__1352 小时前
C语言 | Leetcode C语言题解之第211题添加与搜索单词-数据结构设计
c语言·leetcode·题解
only-lucky4 小时前
C语言:高级并发操作(信号)
c语言·开发语言
GSDjisidi4 小时前
日本IT-SIER/SES的区别详情、契约形态等
java·大数据·c语言·c++·php
YoungMLet4 小时前
【QT】多元素控件
c语言·开发语言·c++·qt·ui
Dalao_zzl5 小时前
C语言实现的冒泡排序算法的示例程序
c语言·算法·排序算法
悄悄敲敲敲5 小时前
栈的实现详解
c语言·开发语言·数据结构·c++·算法·链表·线性回归
安於宿命6 小时前
0/1背包问题总结
c语言·c++·算法·leetcode·动态规划