C 高级编程day02 函数的调用模型与指针强化

文章目录

一、宏函数

1.如何定义宏函数

  1.1 例子:#define MYADD(x,y) ((x) + (y)),所以定义的语法为:

  #define 函数名 函数操作

  定义的宏函数在预处理阶段会被展开,为了保证运算的正确性,通常会在每个数据项和函数表达式加上小括号。

  比如说,定义了一个加法函数 x+y,函数操作我们是可以不加括号这样写,但是如果遇到了运算级别比加法更高的,就会出错。就像下面的代码,在预处理阶段 MYADD(10,30) * 10 就会被替换为 10+30*10,这样就出错了,与本意不符。所以,一般要加上小括号修饰。

void test_1()
{
    printf("%d\n",MYADD(10,30)*10);
}

2.什么时候使用宏函数

  将一些频繁短小的函数 写成宏函数。

3. 宏函数优点:

  以空间换时间,因为普通函数有入栈、出栈时间开销,而宏函数没有,在预处理阶段就已经被替换为了,宏函数定义的操作。

二、函数的调用模型

1.函数调用流程

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

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

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

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

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

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

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;
}

在这里插入图片描述

2.函数调用惯例

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

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

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

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

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

2.2栈的维护方式

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

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

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

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

2.3 常见的调用惯例

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

2.4 函数变量传递分析

2.4.1 main 函数在栈区开启的内存

  main 函数在栈区开启的内存所有子函数均可以使用。

  这个就是在main函数中定义了一个变量,然后将变量的地址传给子函数使用,这个是可以的。
在这里插入图片描述

2.4.2 main 函数在堆区开启的内存

  main 函数在堆区开启的内存所有子函数均可以使用。

  就是在main函数中使用 malloc 开辟一片内存空间,然后将这片内存的地址传给子函数使用,是可以的。
在这里插入图片描述

2.4.3 子函数1在栈区开启的内存

  子函数1在堆区开启的内存,子函数1和子函数2均可以使用(就是子函数1的子函数)。但是 main或者其他不是子函数1的子函数不能使用。

  这个就是在一个函数中定义了一个局部变量,这个函数结束了,不能将这个变量的地址传给其他不是子函数的函数使用。因为这个局部变量的生命周期就是到定义它的函数的最后,函数体结束了,局部变量的生命周期也就结束了。

在这里插入图片描述

2.4.4 子函数1在堆区开启的内存

  子函数1在堆区开启的内存,堆区开辟的内存生命周期是开辟到程序结束,所以在释放之前,其他的函数都可以使用。
在这里插入图片描述

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

在这里插入图片描述

(1)测试栈的生长方向

void test_1()
{
    int a=10;
    int b=10;
    int c=10;
    int d=10;

    printf("a 的地址:%p\n",&a);
    printf("b 的地址:%p\n",&b);
    printf("c 的地址:%p\n",&c);
    printf("d 的地址:%p\n",&d);

    return ;
}

分析结果:栈底到栈顶的地址变换是从高到低。
在这里插入图片描述

(2)测试内存存放方向

  测试代码:我们用一个4字节整型的变量,存放一个十六进制的数0x11223344,一共有4个字节,那么44占一个字节,33占一个字节…那么现在我们就来测试44是放在低地址中,还是放在高地址中。

void test_2()
{
    int a = 0x11223344;
    char *p = &a;

    printf("第1个字节的地址为:%p,存放的数字为:%x\n",p,*p);
    printf("第2个字节的地址为:%p,存放的数字为:%x\n",p+1,*(p+1));
    printf("第3个字节的地址为:%p,存放的数字为:%x\n",p+2,*(p+2));
    printf("第4个字节的地址为:%p,存放的数字为:%x\n",p+3,*(p+3));
}

  测试结果:44(低位数字) 放在低地址中,33(高位数字)放在高地址中。
在这里插入图片描述
  所以我们可以推出:
在这里插入图片描述

  高位字节数据放在内存高地址,低位字节数据放在内存低地址,也就是常背诵的高高低低,我们也称为 小端对齐。但是要注意,不是所有的机器采用这种方式,不过我们使用的这种小型的计算机一般都是采用小端对齐,大型的计算机服务器可能采用大端对齐。

三、指针强化

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.野指针和空指针

2.1 空指针

  标准定义了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		
}

2.2 野指针

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

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

2.3 什么情况会导致野指针

(1)指针变量未初始化

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

(2)指针释放后未置空

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

  下面就来测试一下,指针释放前后指向的内存地址:

void test_1()
{
    int *p = malloc(sizeof(int)*10);

    printf("p 存放的地址为:%p\n",p);

    free(p);
    printf("p 存放的地址为:%p\n",p);
    return ;
}

  查看结果:释放前后指向的都是同一片地址。
在这里插入图片描述

(3)指针操作超越变量作用域

  不要返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放。也就是不要返回局部变量的地址

3.间接访问操作符 *

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

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

(1)使用方法

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

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

  1) * 相当通过地址(指针变量的值)找到指针指向的内存,再操作内存

  2) * 放在等号的左边赋值(给内存赋值,写内存)

  3) * 放在等号的右边取值(从内存中取值,读内存)

(2)测试使用方法

//解引用
void test01()
{

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

4.指针的步长

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

  如果 指针的数据类型是n个字节,那么步长就是n个字节

  下面就用 char 型,int 型,float 型,double 型指针来测试,它们的步长分别是1,4,4,8。

(1)测试

struct stu
{   
    char name[51];
    int age;
    char sex;
    double height;
};

void test()
{   
    struct stu s={ "伊丽莎白",20,'F',167.8 };
    
    // 用4种指针找到身高的属性,本质上是地址的偏移
    int offset_char = 0; // 定义偏移量
    
    // 1.用char *,char类型1个字节,所以步长为1
    offset_char = offsetof(struct stu,height);
    char *p_char = &s;
    
    printf("char * 的方式:s.height = %lf\n",\
    *((double *)(p_char + offset_char)) );
    
    // 2.用int *,int类型4个字节,步长为4
    int offset_int = offset_char/4; 
    int *p_int = &s;
    printf("int * 的方式:s.height = %lf\n",\
    *((double *)(p_int+offset_int)) );

    // 3.用float * , float类型4个字节,步长为4
    int offset_float = offset_char/4;
    float *p_float = &s;
    printf("float * 的方式:s.height = %lf\n",\
    *((double *)(p_float+offset_float)) );

    // 4.用double * ,double类型8个字节,步长为8
    int offset_double = offset_char/8;
    double *p_double = &s;
    printf("double * 的方式:s.height = %lf\n",\
    *(p_double+offset_double));
}

测试结果:
在这里插入图片描述

(2)补充

  (1)要注意+1之后跳跃的字节数是多少,看指针的对应的数据类型。

  (2)解引用的另一个作用就是 解出的字节数(从内存中读取多少个字节的数据),也就是:是什么数据类型的指针,就从数据类型对应的字节数的内存中读取出数据。
拿前面的例子来说明 :

	double *p_double = &s;
  
    *(p_double);

  double 数据类型的指针,double 数据类型是8个字节,所以一次从 p_double 指向的内存中读取8个字节的数据。

  (3)通过 offsetof( 结构体名称, 属性) 找到属性对应的偏移量,单位是1个字节。offsetof 引入头文件 #include<stddef.h>

5.指针的意义——间接赋值

5.1 间接赋值的三大条件

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

  2)建立关系

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

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

5.2 间接赋值:从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);
}

5.3 间接赋值:从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");
	}
}

5.4 间接赋值的推论

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

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

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

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

6.指针做函数参数

  指针做函数参数,要注意指针存放的是什么内容,具备输入和输出特性:

  (1)输入:主调函数分配内存

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

6.1 输入特性

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

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

6.2 输出特性

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

四、字符串指针强化

1.字符串指针做函数参数

1.1 字符串基本操作

//字符串基本操作
//字符串是以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));
}

(1)注意:八进制和十六进制转义字符

  在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.2 字符串拷贝功能实现

//拷贝方法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++){}
}

1.3 字符串反转——while (begin < end)

  这里要注意一个地方,就是什么时候停止反转,用这个while (begin < end),就可以避免一些临界的讨论。一开始我没有想到用这个方法,而是取中间值,哎,傻啊。

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.字符串的格式化

2.1 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);
}

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以外的所有字符
//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.越界

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

2.p++指针叠加会不断改变指针指向

  p++ —》p=p+1;

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

3.返回局部变量地址

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

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

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

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

六、二级指针

1.二级指针基本概念

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

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

  它们如下图进行内存分配:
在这里插入图片描述
  假定我们又有了第3个变量,名叫c,并用下面这条语句对它进行初始化:

c = &b;

  它在内存中的大概模样大致如下:
在这里插入图片描述
  问题是:c的类型是什么?显然它是一个指针,但它所指向的是什么?变量b是一个“指向整型的指针”,所以任何指向b的类型必须是指向“指向整型的指针”的指针,更通俗地说,是一个指针的指针。

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

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

int **c = &b;

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

2.二级指针做形参输入特性

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

//打印数组
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+0) = &a1
	arr[1] = &a2;// 相当于 *(arr+1) = &a2
	arr[2] = &a3;
	arr[3] = &a4;
	arr[4] = &a5;

	print_array(arr,n);

	free(arr);
	arr = NULL;
}

3.二级指针做形参输出特性

//被调函数,由参数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;
	}
}

4.多级指针

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

//分配内存
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);
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值