C语言提高篇(二)

文章详细阐述了C语言中指针的概念,包括解引用的本质、堆栈与堆的指针管理,以及memcpy函数的工作原理。同时讨论了指针作为函数参数的特性,如数组退化为指针的情况。此外,文章还提到了指针在内存分配、交换函数和文件读取中的应用,强调了指针在修改和传递变量值时的注意事项,包括深拷贝与浅拷贝的问题。最后,文章通过示例展示了如何处理结构体数组和避免内存泄漏。
摘要由CSDN通过智能技术生成

P21 指针的基本概念

指针与普通变量的区别就是他多了一个解引用,而C语言实现解引用的本质原因是因为指针里面保存了一个地址,解引用的本质就是取出指针中保存地址当中的那个值。 

 

 在堆栈开辟的指针使用完之后直接置为NULL即可,而在指向堆的指针需要不在使用free掉之后再将指针置为NULL

 等号左边是左值,等号右边是右值

 memcpy第二个参数无所谓你什么类型,他最终都会给你转成char*类型

int a = 10;
00911F2F  mov         dword ptr [ebp-0Ch],0Ah  
	char* p1 = (char*)&a;
00911F36  lea         eax,[ebp-0Ch]  
00911F39  mov         dword ptr [ebp-18h],eax  
	int* p = (int*)a;
00911F3C  mov         eax,dword ptr [ebp-0Ch]  
00911F3F  mov         dword ptr [ebp-24h],eax  
	p = (int*)p1;
00911F42  mov         eax,dword ptr [ebp-18h]  
00911F45  mov         dword ptr [ebp-24h],eax  

由上可知,强制类型转换只是为了欺骗编译器,他们本身的值依旧不变

unsigned char buffer[1024] = { 0 };
00401F32  push        400h  
00401F37  push        0  
00401F39  lea         eax,[buffer]  
00401F3F  push        eax  
00401F40  call        _memset (0401181h)  
00401F45  add         esp,0Ch  
	int a = 0xaabbccdd;
00401F48  mov         dword ptr [a],0AABBCCDDh  
	memcpy(buffer + 1, &a, 4);
00401F52  push        4  
00401F54  lea         eax,[a]  
00401F5A  push        eax  
00401F5B  lea         ecx,[ebp-407h]  
00401F61  push        ecx  
00401F62  call        _memcpy (0401500h)  
00401F67  add         esp,0Ch  
	int num = *(buffer + 1);
00401F6A  movzx       eax,byte ptr [ebp-407h]  
00401F71  mov         dword ptr [num],eax  
	printf("%x\n", *(buffer + 1));
00401F77  movzx       eax,byte ptr [ebp-407h]  
00401F7E  push        eax  
00401F7F  push        offset string "%x\n" (0409BD4h)  
00401F84  call        __empty_global_delete (04014F1h)  
00401F89  add         esp,8  
	printf("%x\n", *(int*)(buffer + 1));
00401F8C  mov         eax,dword ptr [ebp-407h]  
00401F92  push        eax  
00401F93  push        offset string "%x\n" (0409BD4h)  
00401F98  call        __empty_global_delete (04014F1h)  
00401F9D  add         esp,8  
}

 如果将unsigned char改成char buffer:

 其实0xffffffdd也就是0xdd是编译器为了提高效率进行了优化

movsx是对有符号数进行的拓展,movzx是对无符号数进行拓展

而溢出是指:

mov al,0x10
add al,0xFF  

超过了最大容量溢出来了,而0xdd显然没有溢出

P23 指针的间接赋值 

 

 指针的类型决定了指针的步长,指针永远存储的都是一个数据的首地址,编译器怎么知道应该从首地址取多少字节的数据也取决于指针的类型。

P24 指针的输入输出特性

 从lea ecx,[arr]可以看出:数组名作函数参数就会退化为指向数组首元素的指针,相当于将&arr[0]作为函数参数进行传递。

int iArr[] = { 1,2,3,4,5 };
00B61AAF  mov         dword ptr [iArr],1  
00B61AB6  mov         dword ptr [ebp-18h],2  
00B61ABD  mov         dword ptr [ebp-14h],3  
00B61AC4  mov         dword ptr [ebp-10h],4  
00B61ACB  mov         dword ptr [ebp-0Ch],5  
	//字符串默认是const char*类型的数据
	const char* arr[] = {
		"aaa",
00B61AD2  mov         dword ptr [arr],offset string "aaa" (0B69B30h)  
		"bbb",
00B61AD9  mov         dword ptr [ebp-30h],offset string "bbb" (0B69B34h)  
		"ccc",
00B61AE0  mov         dword ptr [ebp-2Ch],offset string "ccc" (0B69BD8h)  
		"ddd"
00B61AE7  mov         dword ptr [ebp-28h],offset string "ddd" (0B69BDCh)  
	};
	int len = sizeof(arr) / sizeof(arr[0]);
00B61AEE  mov         dword ptr [len],4  
	printStringArray(arr, len);
00B61AF5  mov         eax,dword ptr [len]  
00B61AF8  push        eax  
00B61AF9  lea         ecx,[arr]  
00B61AFC  push        ecx  
00B61AFD  call        std::basic_ostream<char,std::char_traits<char> >::_Sentry_base::_Sentry_base (0B6150Ah)  
00B61B02  add         esp,8  
}

运行结果:

 输出特性:被调函数分配内存,主调函数使用内存

交换函数:

void doChange(int* x, int* y) {
	int tmp = 0;
	tmp = *x;
	*x = *y;
	*y = tmp;
}

int x = 10;
int y = 20;
doChange(&x, &y);

int tmp = 0;
00721995  mov         dword ptr [tmp],0  
	tmp = *x;
0072199C  mov         eax,dword ptr [x]  
0072199F  mov         ecx,dword ptr [eax]  
007219A1  mov         dword ptr [tmp],ecx  
	*x = *y;
007219A4  mov         eax,dword ptr [x]  
007219A7  mov         ecx,dword ptr [y]  
007219AA  mov         edx,dword ptr [ecx]  
007219AC  mov         dword ptr [eax],edx  
	*y = tmp;
007219AE  mov         eax,dword ptr [y]  
007219B1  mov         ecx,dword ptr [tmp]  
007219B4  mov         dword ptr [eax],ecx  

判断以下程序是否存在问题:

#include<iostream>
using namespace std;
#include<string>
#pragma warning(disable:4996)
 
void getMem(char* p, int n) {
	p = (char*)malloc(n);
}
 
 
int main() {
 
	char* p = NULL;
	getMem(p, 100);
	strcpy(p, "hello");
	cout << p << endl;
	free(p);
	return 0;
}

运行结果:

 正确写法:

void allocSpace(char** tmp) {
	
	char* p = (char*)malloc(100);
	memset(p, 0, 100);
	strcpy(p, "hello world");
	*tmp = p;
	
}


void test() {

	char* p = NULL;
	allocSpace(&p);
	printf("%s\n", p);
}

总结:想要通过函数修改当前栈中的变量里保存的值,一定要传递该变量的地址

 如果传递指针的目的就是为了打印,即不修改他的内容,那么应当在前面添加const关键字,如果你传递指针的目的就是为了修改变量的话,就不用加const了。

但是这样也是只能防止你无意间修改了传递过来的变量,如果你故意去修改也是完全可以实现的。

读取文件一行使用的fgets()函数:

        通俗来讲的话,fgets()函数的作用就是用来读取一行数据的。但要详细且专业的说的话,fgets()函数的作用可以这么解释:从第三个参数指定的流中读取最多第二个参数大小的字符到第一个参数指定的容器地址中在这个过程中,在还没读取够第二个参数指定大小的字符前,读取到换行符'\n'或者需要读取的流中已经没有数据了。则提前结束,并把已经读取到的字符存储进第一个参数指定的容器地址中。

在正常情况下fgets()函数的返回值和它第一个参数相同。即读取到数据后存储的容器地址。但是如果读取出错或读取文件时文件为空,则返回一个空指针。


 fgets()函数的运行流程大概是这样子的:

当系统调用这个函数的时,系统便会阻塞等待用户的输入,直到用户输入回车符’\n’才返回程序。然后用户输入的内容会被系统放进输入缓存区里面,fgets()函数便会进来读取其“第二个参数减1(为什么减1后面说)”个字节存进它第一个参数指向的内存地址中,如果在还没读取够需要的字节大小前读取到换行符’\n’则提前返回。 

小案例:将文件当中的内容读取到申请的堆空间当中

#include<stdio.h>
#include<iostream>
using namespace std;
#pragma warning(disable:4996)

int getFileLines(FILE* fp) {

	char buffer[1024] = { 0 };
	int lines = 0;
	while (fgets(buffer, 1024, fp) != NULL) {
		++lines;
	}
	//恢复文件指针
	fseek(fp, 0, SEEK_SET);
	return lines;
}

void getFileData(FILE* fp , char** pArray) {
	char buffer[1024] = { 0 };
	int line = 0;
	while (fgets(buffer, 1024, fp) != NULL) {
		int len = strlen(buffer) + 1;
		char* pData = (char*)malloc(len);
		if (!pData) {
			return;
		}
		strcpy(pData, buffer);
		pArray[line++] = pData;
		memset(buffer, 0, 1024);
	}
}

void printString(char** pArray,int lines) {
	for (int i = 0; i < lines; ++i) {
		printf("%s\n", pArray[i]);
	}
}

//释放堆内存

void freeAllocMem(char*** pArray,int lines) {
	for (int i = 0; i < lines; i++) {
		if ((*pArray)[i] != NULL) {
			free((*pArray)[i]);
			(*pArray)[i] = NULL;
		}
	}
	free(*pArray);
	*pArray = NULL;
}

void test() {

	//打开文件
	FILE* fp = fopen("./test.txt", "r");
	if (!fp) {
		printf("文件打开失败!\n");
		return;
	}
	//获取文件行数
	int lines=getFileLines(fp);
	cout << lines << endl;

	//将文件内容放到申请的堆空间中
	char** pArray = (char**)malloc(sizeof(char*) * lines);
	getFileData(fp, pArray);

	//打印缓冲区数据
	printString(pArray, lines);

	//释放堆内存
	freeAllocMem(&pArray, lines);
	cout << pArray << endl;
}

int main() {
	
	test();
	
	return 0;
}

出现如下错误:

 很明显是数组的下标越界

这里编译器理解为先pArray[i]然后再取*,应当修改为先取*,即先(*pArray),然后(*pArray)[i]

 free(pArray),free掉的是pArray指向的地址,而不是pArray本身的地址!!!

pArray[i]是指pArray指向的内存地址里面保存的内容

我们可以利用位运算来对C语言的个别位进行操作,以实现对变量的精确控制

奇数的最后一位是1,偶数最后一位是0:

void test() {
	
	//判断一个数的奇偶性
	int num = 127;
	if ((num & 1 )== 0) {
		printf("%d是偶数\n", num);
	}
	else {
		printf("%d是奇数\n", num);
	}
	//将一个数置零
	num &= 0;
	printf("num=%d\n", num);
}

   异或运算的特性:

 移位运算:

 0101        5

1010        10 ,所以左移几位相当于*2^n

 注:1、数组名是一个常量指针      2、数组的下标可以是负数 arr[-1]会被翻译成*(arr-1)

 打印二维数组:

//方法一
void printDoubleArr(int(*arr)[3], int len1, int len2) {
	
	for (int i = 0; i < len1; ++i) {
		for (int j = 0; j < len2; ++j) {
			printf("%d ", *(*(arr + i) + j));
		}
		printf("\n");
	}
	printf("\n");
}

//方法二
void printDoubleArr2(int arr[3][3], int len1, int len2) {

	for (int i = 0; i < len1; ++i) {
		for (int j = 0; j < len2; ++j) {
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}
	printf("\n");
}

void test() {
	
	//打印二维数组
	int arr[3][3] = {
		{1,2,3},
		{4,5,6},
		{7,8,9}
	};
	printDoubleArr(arr, 3, 3);
	printDoubleArr2(arr, 3, 3);
}

选择排序:

1、对int类型的数据进行排序

void printArr(int arr[], int len) {
	for (int i = 0; i < len; ++i) {
		printf("%d ", arr[i]);
	}
	printf("\n");
}

void selectSort(int *arr, int len) {
	for (int i = 0; i < len-1; ++i) {
		int minIndex = i;
		for (int j = i + 1; j < len; ++j) {
			if (arr[j] < arr[minIndex]) {
				minIndex = j;
			}
		}
		if (minIndex != i) {
			swap(arr[i], arr[minIndex]);
		}
	}
	printArr(arr, len);
}

void test() {
	
	//数组名就是指向首元素类型的指针
	//接收数组类型参数,就看数组名是什么类型就定义什么类型的形参
	int arr[] = {
		1,3,7,9,4,5,6,8,2
	};
	int len = sizeof(arr) / sizeof(int);
	printArr(arr, len);
	selectSort(arr, len);
	
}

2、对字符串类型的数据进行排序

void printArr(const char** arr, int len) {
	for (int i = 0; i < len; i++) {
		printf("%s ", arr[i]);
	}
	printf("\n");
}

void selectSort(const char** arr, int len) {
	for (int i = 0; i < len - 1; ++i) {
		int minIndex = i;
		for (int j = i + 1; j < len; ++j) {
			if (strcmp(arr[j], arr[minIndex]) < 0) {
				minIndex = j;
			}
		}
		if (minIndex != i) {
			swap(arr[i], arr[minIndex]);
		}
	}
	printArr(arr, len);
}

void test() {
	
	const char* arr[] = {
		"ggg","ddd","ccc","bbb","aaa"
	};
	int len = sizeof(arr) / sizeof(char*);
	printArr(arr, len);
	selectSort(arr, len);
}

1、数组名就是指向首元素类型的指针
2、接收数组类型参数,就看数组名是什么类型就定义什么类型的形参

3、通过函数操作数组直接可以修改数组中的数据,因为传递的是指向首元素的指针!

指针可以操作:普通类型的数据、数组、结构体、函数

结构体数组的使用:

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

void printPerson(const Person* person, int len) {

	for (int i = 0; i < len; ++i) {
		printf("Name:%s  Age:%d\n", person[i].name, person[i].age);
	}
}

void test() {

	//在栈上分配结构体数组空间
	Person personArr[] = {
		{"aaa",20},
		{"bbb",30},
		{"ccc",40}
	};

	int len = sizeof(personArr) / sizeof(Person);
	printPerson(personArr, len);

	//在堆上分配结构体数组空间
	//在堆上分配的连续内存空间指针可以直接当数组使用
	Person* ps = (Person*)malloc(sizeof(Person) * 6);
	if (!ps) {
		printf("申请堆内存失败!\n");
		return;
	}
	memset(ps, 0, sizeof(Person) * 6);
	for (int i = 0; i < 6; ++i) {
		sprintf(ps[i].name, "Name_%d", i + 1);
		ps[i].age = 100 + i;
	}
	printPerson(ps, 6);
	free(ps);
	ps = NULL;
}

浅拷贝:直接给结构体赋值 

 直接给结构体赋值,本质上是逐字节拷贝

 同一块内存空间释放两次,程序就会荡掉!!!

浅拷贝存在的问题:

如果结构体内部有指针,并且指针指向一段堆空间,那么如果发生赋值行为的时候就会出现两个问题:1、同一块堆空间被释放两次 2、发生内存泄漏。

即如果结构体内部有指针指向堆内存,就不能够使用编译器默认的赋值方式,而应当手动去赋值,就是深拷贝。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值