【C语言】指针

【C语言】指针



空指针和野指针

空指针

标准定义了NULL指针,它作为一个特殊的指针变量,表示不指向任何东西。要使一个指针为NULL,可以给它赋值一个零值。为了测试一个指针是否为NULL,你可以将它与零值进行比较
对指针解引用操作可以获得它所指向的值。但从定义上看,NULL指针并未指向任何东西,因为对一个NULL指针因引用是一个非法的操作,在解引用之前,必须确保它不是一个NULL指针。
如果对一个NULL指针间接访问会发生什么呢?结果因编译器而异。

  • 不能向NULL或者非法内存拷贝数据
void test01()
{
	char* p = NULL;
	//给p指向的内存区域拷贝内容
	strcpy(p, "11111");		//error

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

野指针

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

  • 野指针出现情况
  1. 指针变量未初始化
  2. 指针释放后未置空
  3. 空指针可以重复释放、野指针不可以重复释放
  4. 指针操作超越变量作用域
void test02()
{
	//1.指针变量未初始化
	int* p;
	printf("%d\n",*p); 

	//2.指针释放后未置空
	char* str = malloc(100);
	free(str);
	//记住释放后置空防止野指针出现
	//str = NULL;

	//3.空指针可以重复释放、野指针不可以重复释放
	free(str);
	
	//4.指针操作超越变量作用域
	int* p = doWork();
	printf("%d", *p);
	
}
int* doWork()
{
	int a = 10;
	int* p = &a;
	return p;
}

间接访问操作符

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

  • 在指针声明时,* 号表示所声明的变量为指针
  • 在指针使用时,* 号表示操作指针所指向的内存空间
    1)* 相当通过地址(指针变量的值)找到指针指向的内存,再操作内存
    2)* 放在等号的左边赋值(给内存赋值,写内存)
    3)* 放在等号的右边取值(从内存中取值,读内存)
int main()
{
	//定义指针
	int* p = NULL;
	//指针指向谁就把谁的地址赋给指针
	int a = 10;
	p = &a;
	*p = 20;//*在=左边,必须确保内存可写
	int b = *p;//*在=右边,从内存中读值
	
	return 0;
}

指针的步长

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

//1.指针的步长代表 指针+1之后跳跃的字节数
void test01()
{
	char* p = NULL;
	printf("%d\n", p);	//0
	printf("%d\n", p + 1);	//1

	double* p2 = NULL;
	printf("%d\n", p2);	//0
	printf("%d\n", p2 + 1);	//8
}
//2.解引用时,解出的字节数量
void test02()
{
	char buf[1024] = { 0 };

	int a = 100;
	memcpy(buf + 1, &a, sizeof(int));
	
	char* p = buf;
	printf("%d\n", *(int*)(p + 1));	//100
}

offsetof是C语言中的一个宏,用于获取结构体中某个成员相对于结构体开始位置的偏移量。这个宏常在<stddef.h>头文件中定义
基本语法:offsetof (type,member_designator)
其中,type是你要查询偏移量的结构体的类型、member_designator是结构体成员名称
返回是size_t类型的值,表示成员在结构体的偏移量,单位字节。

//步长练习,自定义数据练习
struct Person
{
	char a;		//0~3
	int b;		//4~7
	char buf[64];		//8~71
	int d;		//72~75

};

void test03()
{
	struct Person p = { 'a',10,"hello world",20 };
	printf("d属性的偏移量:%d\n", offsetof(struct Person, d));	//72

	printf("d属性的值为:%d\n", *(int*)((char*)&p + offsetof(struct Person, d)));	//20
}


指针间接赋值

  • 间接赋值三大条件 :
  1. 一个普通变量 和 指针变量 或 一个实参和一个形参
  2. 建立关系
  3. 用* 操作内存
void changeValue(int *a)	//int * a = &a2;
{
	*a = 1000;
}


void test01()
{
	int a = 10;
	int* p = NULL;

	p = &a;
	*p = 100;

	int a2 = 10;
	changeValue(&a2);
	printf("%d\n", a2);

}

指针做函数参数的输入输出特性

  • 输入特性:主调函数分配内存,被调函数使用
//输入特性:主调函数分配内存,被调函数使用
void func(char* p)
{
	strcpy(p, "hello world");
}

void test01()
{
	//在test01中分配了内存,分配在栈上
	char buf[1024] = { 0 };

	func(buf);

	printf("%s\n", buf);

}
void printString(char * str)
{
	printf("%s\n", str + 6);

}

void test02()
{
	//分配在堆区
	char * p = malloc(sizeof(char)* 64);
	memset(p, 0, 64);
	strcpy(p, "hello world");
	printString(p);

	if (p != NULL)
	{
		free(p);
		p = NULL;
	}
}
  • 输出特性:在被调函数中分配内存,主调函数使用
//输出特性:在被调函数中分配内存,主调函数使用
void allocateSpace(char** pp)
{
	char* str = malloc(sizeof(char*) * 64);
	memset(str, 0, 64);
	strcpy(str, "hello world");

	*pp = str;
}


void test03()
{
	char* p = NULL;

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


字符串指针强化

字符串基本操作

字符串是以0或者’\0’结尾的字符数组,(数字0和字符’\0’等价)

void test01()
{
	//字符串结束标志位 \0
	char str1[] = { 'h','e','l','l','o','\0' };
	printf("%s\n", str1);


	char str2[100] = { 'h','e','l','l','o','\0' };
	printf("%s\n", str2);


	char str3[] = "hello";
	printf("%s\n", str3);
	printf("sizeof str:%d\n", sizeof(str3));	//6
	printf("strlen str:%d\n", strlen(str3));	//5


	char str4[100] = "hello";
	printf("sizeof str:%d\n", sizeof(str4));	//100
	printf("strlen str5:%d\n", strlen(str4));	//5


	//strlen识别'\0'
	char str5[] = "hell0\0world";
	printf("sizeof str %d\n", sizeof(str5));	//12
	printf("strlen str5:%d\n", strlen(str4));	//5 

	// \012 八进制
	char str6[] = "hello\012world";
	printf("%s\n", str6);
	printf("sizeof str6:%d", sizeof(str6));		//12	
	printf("strlen str6:%d", strlen(str6));		//11
}

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

上述代码中的\012为八进制转为十进制为10对应ASCII码字符 ‘\n’

字符串拷贝实现

  • 利用[] 实现
void copyString01(char* dest, char *src)
{
	int len = strlen(src);
	
	for (int i=0; i < len; i++)
	{
		dest[i] = src[i];
	}
	dest[len] = '\0';
}

  • 利用字符串指针
void copyString02(char*dest ,char *src)
{
	while (*src != '\0')
	{
		*dest = *src;

		dest++;
		src++;
	}
	*dest = '\0';
}

  • 方法三
void copyString03(char* dest, char* src)
{
	while(*dest++=*src++){}
}

src='\0’的时候表达式dest='\0’退出循环

字符串翻转

  • 利用[]
void reverseString01(char* str)
{
	int len = strlen(str);
	int start = 0;
	int end = len - 1;

	while (start < end)
	{
		char temp = str[start];
		str[start] = str[end];
		str[end] = str[temp];

		start++; 
		end--;
	}
}
  • 利用字符串指针
void copeString02(char* str)
{
	int len = strlen(str);
	char* start = str;
	char* end = str + len - 1;

	while (start < end)
	{
		char temp = *start;
		*start = *end;
		*end = temp;
		
		start++;
		end--;
	}
}

字符串的格式化

sprintf

#include <stdio.h>
int sprintf(char *str, const char *format, …);
功能:
根据参数format字符串来转换并格式化数据,然后将结果输出到str指定的空间中,直到 出现字符串结束符 ‘\0’ 为止。
参数:
str:字符串首地址
format:字符串格式,用法和printf()一样
返回值:
成功:实际格式化的字符个数
失败: - 1

void test01()
{
	//1.格式化字符串
	char buf1[1024];
	memset(buf1, 0, 1024);
	sprintf(buf1, "今天 %d 年 %d 月 %d 日", 2024, 4, 10);
	printf("%s\n", buf1);	//今天 2024 年 4 月 10 日


	//2.拼接字符串
	char buf2[1024];
	memset(buf2, 0, 1024);
	char str1[] = "hello";
	char str2[] = "world";
	int len = sprintf(buf2, "%s %s", str1, str2);	//返回值是字符串长度 不包含\0
	printf("buf2:%s len:%d\n", buf2, len);	//buf2:hello world len:11


	//3.数字转字符串
	char buf3[1024];
	memset(buf3, 0, 1024);
	int num = 100;
	sprintf(buf3, "%d", num);
	printf("buf3:%s\n", buf3); 


	//4.设置宽度 向右对齐
	char buf4[1024];
	memset(buf4, 0, 1024);
	sprintf(buf4, "%8d", num);	
	printf("buf4:%s\n", buf4);	//buf4:     100


	//5.设置宽度 向左对齐
	char buf5[1024];
	memset(buf5, 0, 1024);
	sprintf(buf5,"%-8d", num);
	printf("buf5:%sa\n",buf5);	//buf:100     a


	//6.转成16进制字符串 小写
	char buf6[1024];
	memset(buf6, 0, 1024);
	sprintf(buf6, "0x%x", num);
	printf("buf6:%s\n", buf6);	//buf6:0x64

	//7.转为8进制字符串
	char buf7[1024];
	memset(buf7, 0, 1024); 
	sprintf(buf7,"0%o", num); 
	printf("buf7:%s", buf7);	//buf7:0144
}

sscanf

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

格式作用
%*s或%*d跳过数据
%[width]s读指定宽度的数据
%[a-z]匹配a到z中任意字符(尽可能多的匹配)
%[aBc]匹配a、B、c中一员,贪婪性
%[^a]匹配非a的任意字符,贪婪性
%[^a-z]表示读取除a-z以外的所有字符
// %*s或%*d	跳过数据
void test01()
{
	char* str = "12345abcde";
	char buf[1024] = { 0 };
	sscanf(str, "%*d%s", buf);
	printf("%s\n", buf);
}


void test02()
{
	char* str = "abcd1e12345";
	char* str2 = "123 5abcde";
	char buf[1024];
	char buf2[1024];
	//sscanf(str, "%*s%d", buf);	//error
	sscanf(str, "%*[a-z]%s", buf);	//1e12345	//忽略遇到不匹配的代表忽略结束
	sscanf(str2, "%*d%s", buf2);	//5abcde	//忽略遇到空格或者 \t 代表忽略结束

	printf("%s\n", buf);
	printf("%s\n", buf2);
}



//%[width]s 读指定宽度的 数据
void test03()
{
	char* str = "1234567abcdef";
	char buf[1024] = { 0 };
	sscanf(str, "%9s", buf);
	printf("%s\n", buf);		//1234567ab
}



//%[a-z]	匹配a到z中任意字符(尽可能多的匹配)
void test04()
{
	char* str = "12345ababac";
	char buf[1024] = { 0 };
	sscanf(str, "%*d%[a-c]", buf);	 //只要匹配失败,那么就不继续匹配了
	printf("%s\n", buf);		//ababac
}


void test05()
{
	char* str = "12345ababac";
	char buf[1024] = { 0 };
	sscanf(str, "%[0-9]", buf);
	printf("%s\n", buf);		//12345
}



//%[aBc]     匹配a、B、c中的一员,贪婪性
void test06()
{
	char* str = "aCbCdEbfg";
	char buf[1024] = { 0 };
	sscanf(str, "%[abC]", buf);
	printf("%s\n", buf);		//aCbC
}



//%[^a]		匹配非a的任意字符,贪婪性
void test07()
{
	char* str = "abcdegf";
	char buf[1024] = { 0 };
	sscanf(str, "%[^Cg]", buf);		//abcde
	printf("%s\n", buf);
}



//%[^a-z]	表示读取除a-z以外的所有字符
void test08()
{
	char* str = "abcCdef123456";
	char buf[1024] = { 0 };

	sscanf(str, "%[^0-9]", buf);
	printf("%s\n", buf);		//abcCdef
}

//练习1
void test09()
{
	char* ip = "119.0.0.9";
	int num1 = 0; 
	int num2 = 0;
	int num3 = 0;
	int num4 = 0;

	sscanf(ip, "%d.%d.%d.%d", &num1, &num2, &num3, &num4);

	printf("%d\n", num1); 
	printf("%d\n", num2);
	printf("%d\n", num3);
	printf("%d\n", num4);
}

//练习2
void test10()
{
	char* str = "abcdef*lengbaibai@123456";
	char buf[1024] = { 0 };

	sscanf(str, "%*[^*]*%[^@]", buf);
	printf("%s\n", buf);		//lengbaibai
}


//已给定字符串为: helloworld@leng.baibai,请编码实现helloworld输出和leng.baibai输出。
void test11()
{
	char* str = "helloworld@leng.baibai";

	char buf1[1024] = { 0 };
	char buf2[1024] = { 0 };

	sscanf(str, "%[a-z]%*[@]%s", buf1, buf2);

	printf("%s\n", buf1);
	printf("%s\n", buf2);
}

查找子串

int myStrstr(char*str,char*subStr)
{
	int num = 0;
	while (*str != '\0')
	{
		if (*str != *subStr)
		{
			num++;
			str++;
			continue;
		}

		//创建两个临时指针 做二次对比
		char* tmper = str;
		char* temSubstr = subStr;

		while (*temSubstr != '\0')
		{
			if (*tmper != *temSubstr)
			{
				//匹配失败
				num++;
				str++;
				break;
			}
			tmper++;
			temSubstr++;
		}


		if (*temSubstr == '\0')
		{
			//匹配成功
			return num; 
		}
	}
	return -1;
}


void test01()
{
	char* str = "abdnsifiawi";
	int ret = myStrstr(str, "nsi");
	if (ret != -1)
	{
		printf("找到了子串,位置为:%d\n", ret);

	}
	else
	{
		printf("未找到子串\n");
	}
}


指针易错点

在堆空间上开辟内存指针移动后释放导致错误。

void test01() //error
{ 
	char* p = malloc(sizeof(char) * 64); 
	for (int i = 0; i < 10; i++) { 
		*p = i + 97; 
		printf("%c\n", *p); 
		p++;   
	}
	if (p != NULL) 
	{
		free(p);
	}
}

void test02() //true
{
	char* p = malloc(sizeof(char) * 64);
	char* pp = p;	//	通过创建临时指针操作内存,防止出错
	for (int i = 0; i < 10; i++) {
		*pp = i + 97;
		printf("%c\n", *pp);
		pp++;
	}
	if (p != NULL)
	{
		free(p);
	}
}

const的使用场景

const 使用 修饰形参 防止误操作

struct Person
{
	char name[64];	//0~63
	int age;		//64~67
	int Id;			//68~71
	double score;	//72~79
};

//将 struct Person p 改为struct Person * p		节省资源
//const 使用 修饰形参 防止误操作
void showPerson(const struct Person *p)
{
	//p->age = 100;
	printf("姓名: %s  年龄: %d  学号 %d  分数 %f\n", p->name, p->age, p->Id, p->score);

}

void test01()
{
	struct Person p = { "lengbaibai",18,1,100 };

	showPerson(&p);
}

二级指针做函数参数的输入输出特性

  • 输入特性:二级指针做形参输入特性是指由主调函数分配内存。
void printArray(int** pArray, int len)
{
	for (int i = 0; i < len; i++)
	{
		printf("%d\n", *pArray[i]);
	}
}
void test01()
{
	//在堆区创建
	int** pArray = malloc(sizeof(int*) * 5);
	
	//在栈上创建5个数据
	int a1 = 10;
	int a2 = 20;
	int a3 = 30;
	int a4 = 40;
	int a5 = 50;


	pArray[0] = &a1;
	pArray[1] = &a2;
	pArray[2] = &a3;
	pArray[3] = &a4;
	pArray[4] = &a5;

	//打印数组
	printArray(pArray, 5);

	//释放堆区数据
	if (pArray != NULL)
	{
		free(pArray);
		pArray = NULL;
	}
}


void freeSpace(int* pArray, int len)
{
	for (int i = 0; i < 5; i++)
	{
		free(pArray[i]);
		pArray[i] = NULL;
	}
}

void test02()
{
	//创建在栈区
	int* pArray[5];

	for (int i = 0; i < 5; i++)
	{
		pArray[i] = malloc(4);
		*(pArray[i]) = 100 + i;

	}
	printArray(pArray, 5);
	//释放堆区
	freeSpace(pArray, 5);
}
  • 输出特性:二级指针做参数的输出特性是指由被调函数分配内存。
void allocateSpace(int ** p)
{
	int* temp = malloc(sizeof(int) * 10);
	for (int i = 0; i < 10; i++)
	{
		temp[i] = 100 + i;

	}
	*p = temp;

}

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

void freeSpace(int** pArray)
{
	if (*pArray != NULL)
	{
		free(*pArray);
		*pArray = NULL;
	}
}


void test01()
{
	int* p = NULL;
	allocateSpace(&p);

	printArray(&p,5);  

	freeSpace(&p);
	
	if (p == NULL)
	{
		printf("空指针\n");
	}
	else
	{
		printf("野指针\n");
	}
}

二级指针练习

利用二级指针来实现文件读写

//获取有效行数
int getFileLines(FILE* pfile)
{
	if (pfile == NULL)
	{
		return -1;

	}
	char buf[1024] = { 0 };
	int lines = 0;
	
	while (fgets(buf, 1024, pfile) != NULL)
	{
		//printf("%s\n",buf);
		lines++;
	}

	//将文件光标置首
	fseek(pfile, 0, SEEK_SET);
	return lines;
}


//读取数据放入到pArray中
void readFileData(FILE* pfile, int len, char** pArray)
{
	if (pfile == NULL)
	{
		return;
	}
	if (len <= 0)
	{
		return;
	}
	if (pArray == NULL)
	{
		return;
	}

	char buf[1024] = { 0 };
	int index = 0;
	while (fgets(buf, 1024, pfile) != NULL)
	{
		/*
		aaaaa
		bbbbb
		ccc
		ddd
		efgh
		*/
		int currentLen = strlen(buf) + 1;
		char* currentStrP = malloc(sizeof(char) * currentLen);
		strcpy(currentStrP, buf);
		pArray[index++] = currentStrP;

		memset(buf, 0, 1024);
	}
}


void showFileData(char** pArray, int len)
{
	for (int i = 0; i < len; i++)
	{
		printf("%d行的数据为 %s", i + 1, pArray[i]);
	}
}



void test01()
{
	//文件内容
	/*
	aaaaa
	bbbbb
	ccc
	ddd
	efgh
	*/

	//打开文件
	FILE* pfile = fopen("./test.txt", "r");
	if (pfile == NULL)
	{
		printf("文件打开失败\n");
		return;
	}

	//统计有效行数
	int len = getFileLines(pfile);

	char** pArray = malloc(sizeof(char*) * len);

	//读取文件中的数据并且放入到 pArray 中
	readFileData(pfile, len, pArray);

	//读取数据
	showFileData(pArray, len);

	//释放堆区内容
	for (int i = 0; i < len; i++)
	{
		if (pArray[i] != NULL)
		{
			free(pArray[i]);
			pArray[i] = NULL;
		}
	}
	free(pArray);
	pArray = NULL;

	//关闭文件
	fclose(pfile);
}

总结

到这里这篇文章的内容就结束了,谢谢大家的观看,如果有好的建议可以留言喔,谢谢大家啦!

  • 39
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值