【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避免,而只能通过养成良好的编程习惯来尽力减少。对野指针进行操作很容易造成程序错误。
- 野指针出现情况
- 指针变量未初始化
- 指针释放后未置空
- 空指针可以重复释放、野指针不可以重复释放
- 指针操作超越变量作用域
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
}
指针间接赋值
- 间接赋值三大条件 :
- 一个普通变量 和 指针变量 或 一个实参和一个形参
- 建立关系
- 用* 操作内存
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);
}
总结
到这里这篇文章的内容就结束了,谢谢大家的观看,如果有好的建议可以留言喔,谢谢大家啦!