文章目录
一、指针
指针 == 内存地址
指针定义:数据类型 * p = arr;
如:int** p = arr;
在这里int*表示p指向的数据的数据类型为指针,第二个星号表示p是一个指针。
我把”数据类型 * p“翻译为“什么数据类型的指针”,指针指向什么数据类型,定义的时候就写什么数据类型。
1. 查询/存储数据
//定义时的*是一个标记,代表定义的是一个指针变量,内部存储的是地址。
int a = 10;
int* p = &a; //指针变量的数据类型与指向变量的数据类型相同
//指针变量占用的大小与数据类型无关,与编译器位数有关
//下面的*是解引用运算符,得到地址中的存储的数据。
printf("%d\n", *p);
*p = 20; //可以修改数据从10为20
##思考
如果直接将变量地址赋给另一个变量(不是指针变量),能不能存储地址呢?
不能,数据类型长度不够。
int a = 1;
int p = &a; //不添加指针的标志,非指针变量
int* p1 = &a; //指针变量
printf("%p\n\n", &a);//输出结果为:000000065255FBB4
printf("%p\n", p); //输出结果为:000000005255FBB4
//因为p是int类型变量,32位二进制,故只够存储8位十六进制,不能完全存储地址
printf("%d\n\n", p); //输出结果为:1381366708
//1381366708转化为十六进制就是5255FBB4
printf("%p\n", p1); //输出结果为:000000065255FBB4,即a的地址
printf("%d\n", p1); //输出结果为:1381366708
//%d输出int类型,故将p1中存储的数截取int可承载的位数后转化为了十进制
2. 指针的作用
指针存储的是数据的地址,但是我们可以直接使用变量,为什么还要用指针存储它的地址,再去做使用呢?
简单来说:通过指针,函数能够改变函数外的变量值(引用调用)。
如下4个作用:
-
操作其他函数中的变量;
例:交换两个变量中的值
①不使用指针(传值调用):
void swap(int num1, int num2)//传递的只是a和b的值,将其赋给num1和num2 { //交换的也只是num1和num2的值 int temp = num1; num1 = num2; num2 = temp; } int a = 10; int b = 20; printf("%d, %d\n", a, b); swap(a, b); printf("%d, %d\n", a, b); //运行后发现a、b中的值并没有发生改变
②使用指针:
void swap(int* p1, int* p2)//此时传递的是a和b的地址,p1指向a,p2指向b { //修改的就是a和b的值 int temp = *p1; *p1 = *p2; *p2 = temp; } int a = 10; int b = 20; printf("%d, %d\n", a, b); swap(&a, &b); printf("%d, %d\n", a, b); //成功将a和b中的值进行了交换
-
细节:
函数中变量的生命周期与函数相关,函数结束,变量随之消失。在其他函数中就无法再通过指针使用。若要避免回收,为变量添加static关键字。
int* method() { static int a = 10; return &a; } int* p = method(); printf("%d\n", *p); //如果不使用static,则无法得到10
-
-
函数返回多个值;
因为指针传递的是地址,使用函数对其操作,会改变地址中的值,所以即使函数结束,值的改变也会保留下来。
函数的变量是局部变量,函数体内赋值后并不会改变主函数中变量的值,使用return只能返回一个值。使用指针后改变的就是那个内存地址存储的值,可以改变多个主函数中的变量。
例:求取数组中最大最小值
void newCompare(int arr[], int length, int* max, int* min) { for (int i = 1; i < length; i++) { if (*max < arr[i]) { *max = arr[i]; } } for (int j = 1; j < length; j++) { if (*min > arr[j]) { *min = arr[j]; } } } int arr[] = {1, 2, 3, 4, 5}; int max = arr[0]; int min = arr[0]; int length = sizeof(arr) / sizeof(arr[0]); newCompare(arr, length, &max, &min); printf("数组的最大值为:%d, 最小值为:%d\n", max, min);
-
函数的结果和计算状态分开;
我们不需要再通过return得到我们想要的计算结果,只需要return负责计算状态。
例:计算两数相除的余数
int getRemainder(int num1, int num2, int* res) { if (num2 == 0) { return 1; } *res = num1 % num2; return 0; } int num1 = 10; int num2 = 3; int res = 0; int flag = getRemainder(num1, num2, &res); if (!flag) { printf("两数相除的余数为:%d\n", res); } else { printf("除数不可以为0!\n"); }
-
方便的操作数组和函数
后面数组指针、指针数组及函数指针会详细阐述。
3. 指针的运算
指针的运算,就是对记录的内存地址进行操作,与步长息息相关(运算结果的单位是步长)。
步长:指针移动一次,走了多少个字节。与数据类型有关,步长为数据类型的长度所用的字节数。
int a = 1;
int* p = &a;
int* p1 = p + 1;
printf("%p\n", p); //输出结果为:000000A36B9FFB74
printf("%p\n", p1); //输出结果为:000000A36B9FFB78
//该指针移动的步长为4个字节。
运算中有意义的操作:
①对指针直接加减数字:相当于数组索引增加(即增加偏移量);
②指针之间进行减法操作:得到间隔步长。
二、指针高级
1. 野指针和悬空指针
-
野指针
指针指向的空间未分配(指向了未知的地址)。
int a = 10; int* p1 = &a; printf("%p\n", p1); printf("%d\n", *p1); // 野指针 int* p2 = p1 + 10; printf("%p\n", p2); printf("%d\n", *p2); //p2会指向一个地址,并且可以得到值,但这个地址在该程序中并没有被定义,所以如果对这个地址存储的数据进行操作,可能会影响其他程序的运行。
-
悬空指针
指针指向的空间已分配,但被释放了(指向了已被回收的地址)。
int* method() { int num = 10; //函数结束后num所占用的内存空间被释放 int* p = # return p; } // 悬空指针 int* p3 = method(); printf("%p\n", p3); printf("%d\n", *p3); //运行结果为:17,不是函数中的10
2. 没有类型的指针
背景概念:不同类型的指针之间,不能互相赋值。
但void指针:
-
优点:没有任何类型,可以接受任意指针记录的地址;
-
缺点:无法获取变量里面的数据。
个人理解:没有指定数据类型,就不知道步长是多少,影响取值和加减运算。
int a = 10;
int* p1 = &a;
void* p4 = p1;
printf("%d\n", *p4);
printf("%p\n", p4 + 1);
//这两段打印语句均无法执行
例:书写一个函数交换变量的数据,要求具有通用性。
void swap(void* p1, void* p2, int len)
{
char* pc1 = p1;
char* pc2 = p2;
char temp = 0;
//以字节为单位,一个字节一个字节进行交换
for (int i = 0; i < len; i++)
{
temp = *pc1;
*pc1 = *pc2;
*pc2 = temp;
pc1++;
pc2++;
}
}
3. 二级指针和多级指针
指向指针的指针,可以操作低一级指针记录的地址(更多在数组中进行使用)。
定义时要注意指针数据类型,指针数据类型要跟指向空间中数据类型保持一致。
①可以修改指针记录的内存地址(和解引用指针可以修改变量的值一样);
②获取变量中存储的值(增加解引用次数即可)。
int a = 10;
int b = 20;
int* p1 = &a;
int** p2 = &p1;
printf("%d\n", *p1); //输出为10
*p2 = &b; //将p1指向的地址改为了b的地址(作用1)
printf("%d\n", **p2); //输出为20(作用2)
return 0;
4. 数组指针
指向数组的指针,可以操作数组中的数据。
//遍历数组
int arr[] = {10, 20, 30, 40, 50};
int len = sizeof(arr) / sizeof(arr[0]);
int* p = arr;
for (int i = 0; i < len; i++)
{
printf("%d\n", *p++);
}
-
arr参与运算时,就退化为了指向数组第一个元素的指针;
特殊情况:
- 使用sizeof()函数时,arr是一个整体;
- &arr获取地址时,获取的也是一个整体(比如指针+1,会直接跨过一个数组)。
int arr[] = {10, 20, 30, 40, 50};
printf("%p\n", arr);
printf("%p\n", &arr);
printf("%d\n", sizeof(arr));
printf("%p\n", arr + 1); //步长为一个元素的长度
printf("%p\n", &arr + 1); //步长为整个数组的长度
5. 二维数组&数组指针
二维数组的两种定义格式:
//格式一:
int arr[3][3] =
{
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
//格式二:
//如果一维数组长度不同,这种定义格式更适用
int arr1[3] = {1, 2, 3};
int arr2[3] = {4, 5, 6};
int* arr3[2] = {arr1, arr2}; //数组的数据类型和存储的元素保持一致
-
格式一
(1)索引遍历:
int len1 = sizeof(arr[1]) / sizeof(int); int len2 = sizeof(arr) / sizeof(int) / len1; for (int i = 0; i < len2; i++) { for (int j = 0; j < len1; j++) { printf("%d ", arr[i][j]); } printf("\n"); }
(2)指针遍历:
这里我们使用到数组指针,接下来对其定义格式一步步进行拆解:
int arr[3][3] = { {1, 2, 3}, {4, 5, 6}, {7, 8, 9} }; //指针数据类型定义要和元素类型一致,此处二维数组中存储的是一维数组,所以: int[3] * p = arr; //但方括号一般写在后面,所以: int * p[3] = arr; //但此时看起来像定义了一个指针数组,所以: int (*p)[3] = arr; //对于最终的定义格式,我简单理解为:基础指针定义格式+可表示步长,再补充()
进行遍历:
int(*p)[3] = arr; // p是二维数组的指针,二维数组存储的是一维数组的地址,所以*p是一维数组的地址 for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { printf("%d ", *(*p + j)); } printf("\n"); p++; }
-
格式二
(1)索引便利:
int arr1[3] = { 1, 2, 3 }; int arr2[4] = { 1, 2, 3, 4 }; int arr3[5] = { 1, 2, 3, 4, 5 }; int* arr[3] = { arr1, arr2, arr3 }; int len1 = sizeof(arr1) / sizeof(int); int len2 = sizeof(arr2) / sizeof(int); int len3 = sizeof(arr3) / sizeof(int); int lenArr[3] = {len1, len2, len3}; for (int i = 0; i < 3; i++) { for (int j = 0; j < lenArr[i]; j++) { printf("%d ", arr[i][j]); } printf("\n"); } //为什么上述代码中要先单独将数组的长度求出来呢,不可以在循环内部求取吗? //如下代码(错误方法): int len; for (int i = 0; i < 3; i++) { //64位系统中此处求取的len必是2,因为在定义arr时,arr[i]就已经只是一个内存地址,占8个字节,所以sieof(arr[i]) = 8,并非是数组的整体长度。 len = sizeof(arr[i]) / sizeof(int); for (int j = 0; j < len; j++) { printf("%d ", arr[i][j]); } printf("\n"); }
(2)指针遍历
int arr1[3] = { 1, 2, 3 }; int arr2[3] = { 4, 5, 6 }; int arr3[3] = { 7, 8, 9 }; int* arr[3] = { arr1, arr2, arr3 }; // 我们首先要遍历二维数组中的一维数组再遍历具体的一维数组 // arr中存储的是指针,所以int*,因为我们定义的是指针,所以再加一个* int** p = arr; for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { printf("%d ", *(*p + j)); } printf("\n"); p++; }
6. 数组指针和指针数组
(1)数组指针:指向数组的指针
作用:用于操作数组中的数据。
int* p = arr; // 步长为int的长度,4个字节
int (*p)[5]; // 步长为int*5 = 20个字节
(2)指针数组:存放指针的数组
作用:用于存放指针。
int* p[5]; // 这个数组存放的便是int类型的指针
7. 函数指针
7.1 函数指针
作用:利用函数指针,可以动态的调用函数。
代码示例:
void method1();
int method2(int num1, int num2);
int main()
{
int result;
void (*p1)() = method1;
int (*p2)(int, int) = method2;
p1();
result = p2(1, 2);
printf("%d\n", result);
return 0;
}
void method1()
{
printf("method1\n");
}
int method2(int num1, int num2)
{
printf("method2\n");
return num1 + num2;
}
7.2 函数指针数组
由下面的代码示例我们可以发现函数指针数组的使用细节:
函数指针数组中,函数的返回值以及形参需要完全相同。
例:定义四个函数进行加减乘除运算,键盘录入两个被计算的数字及一个表示运算方式的数字(使用函数指针数组确定函数的使用)。
##思考:
为什么不使用条件判断语句而是使用函数指针数组呢,代码后解释。
int add(int num1, int num2);
int subtract(int num1, int num2);
int multiply(int num1, int num2);
int divide(int num1, int num2);
int main()
{
int (*pArr[4])(int, int) = {add, subtract, multiply, divide};
// 此处使用函数指针数组来存放
int num1;
int num2;
int num3;
int flag;
printf("1:加法\n2:减法\n3:乘法\n4:除法\n\n");
printf("请输入要进行计算的两个数字:");
scanf("%d %d", &num1, &num2);
printf("请输入数字选择运算方式:");
scanf("%d", &flag);
int result = (pArr[flag - 1])(num1, num2);
printf("运算的结果为:%d\n", result);
return 0;
}
int add(int num1, int num2)
{
return num1 + num2;
}
int subtract(int num1, int num2)
{
return num1 - num2;
}
int multiply(int num1, int num2)
{
return num1 * num2;
}
int divide(int num1, int num2)
{
return num1 / num2;
}
思考的解释:
此处只涉及加减乘除四种运算,使用条件判断语句书写代码篇幅较少,但实际项目中可能涉及更多函数的选择使用,使用条件判断语句会大大增加代码的书写量(造成代码冗余、阅读性差等)。使用函数数组指针,可以理解为将函数作为数组的一个元素,直接索引调用即可,代码书写量极少。
三、字符串
1. 定义格式及细节
字符串在底层中是多个字符,以字符数组的形式进行存储,在其末尾’\0’作为结束标记。
所以,字符串最标准但比较麻烦的定义格式:
char str[4] = {'a', 'b', 'c', '\0'};
所以,常用的两种定义格式:
(1)格式一:字符数组 + 双引号
char str1[4] = "abc";
str1[0] = 'A';
(2)格式二:指针 + 双引号
char* str = "abc"; // 将字符串的地址赋值给str
格式一:该形式下底层的字符数组存储在普通内存区中,可读可写
细节:
- 底层存储原理:单个字符组成的字符数组,且末尾添加’\0’结束;
- 关于定义:数组的长度可以不写,如果写需要预留’\0’的空间;
- 关于使用:可以改变字符串内容(因为字符数组可变)。
格式二:该形式下底层的字符数组存储在只读常量区
细节:
底层存储原理:单个字符组成的字符数组,且末尾添加’\0’结束;
底层存储区域:存储在只读常量区;
内容不可修改;
该区域定义的字符串可复用;
理解复用:即当我们定义了相同内容的字符串,会先检查再去占用内存空间,如果已经有相同内容的字符串,则使用相同的地址。
简单讲:以上两种格式,第一种内容可以修改;第二种内容不可以修改但可以复用。
##例1:
我们给出一个例子,键盘录入一个字符串并循环遍历打印。
(1)键盘录入字符串:
方式一:
char* str1 = NULL;
printf("请输入一个字符串:");
scanf("%s", str1); // 注意此处str1本身就是一个指针,不需要再取址
方式二:
char str2[100];
printf("请输入一个字符串:");
scanf("%s", str2); // 注意此处str2本身就是一个指针,不需要再取址
给出判断:方式一是错误的。
方式一使用指针的方式定义字符串,但该种方式字符串存储于只读常量区,内容无法修改,因此无法键盘录入。
方式二使用数组的方式定义字符串,该种方式将字符串存储于内存,可以修改内容。
(2)循环遍历:
复习一点:使用while、do…while、for进行循环遍历前,应先进行挑选,不是每一种循环都可以简单的实现要求。
在此处对键盘录入的字符串进行遍历,我们不知道需要进行循环的次数,但我们知道的是在遍历到’\0’时结束,即知道循环结束的条件,因此使用while循环是更合适的方法。
char* p = str;
// 此处一定要注意定义指针的数据类型是char类型的指针,个人认为需要将指针的数据类型和变量的数据类型保持对应与步长有关。
while (1)
{
char c = *p;
if (c == '\0')
{
break;
}
printf("%c", c);
p++;
}
关于打印:
我们已经知道字符串底层存储原理是一个字符数组,也就是说字符串的变量名就是一个指针,指向这个字符串数组的首位,即第一个字符。
但是在打印字符串时,我们不需要添加解引用,可以理解为是占位符”%s“的作用,遍历了整个字符数组并在识别到‘\0’时停下。
存储多个字符串:
方式一:
char strArr1[5][100] = {"aaa", "bbb", "ccc", "ddd", "eee"};
方式二:
char* strArr2[5] = {"aaa", "bbb", "ccc", "ddd", "eee"};
//我的理解是:strArr2[5]是一个指针数组
##例2:
char strArr1[5][100] = {"aaa", "bbb", "ccc", "ddd", "eee"};
for (int i = 0; i < 5; i++)
{
char* p1 = strArr1[i]; // 这里可以发现数组中存放的还是地址
printf("%s ", p1); // 但字符串打印不需要解引用
// 在这里不添加解引用打印错误,个人理解在进行指针运算时退化为了纯指针,需要解引用
printf("%s ", *(strArr1 + i));
printf("%s\n", strArr1[i]); // 这里还是字符串的指针
}
char* strArr2[5] = { "aaa", "bbb", "ccc", "ddd", "eee" };
for (int i = 0; i < 5; i++)
{
char* p2 = strArr2[i];
printf("%s ", p2);
printf("%s ", *(strArr2 + i));
printf("%s\n", strArr2[i]);
}
##小结:
-
两种定义格式,采用数组形式字符串存储于内存,内容可以修改;采用指针形式字符串存储于只读常量区,内容不可以修改;
-
字符串底层以字符数组的形式存储,结尾有结束符‘\0’,注意预留空间,注意记住’\0’这个条件;
-
字符串数组其实就是二维数组,数组内的元素都是字符串(但其实就是都是指针都是地址);
-
在打印的时候,如果直接进行打印不需要进行解引用;
-
但如果打印的是指针的运算,需要进行解引用;
示例代码: char* strArr[5] = { "abc", "bbb", "ccc", "ddd", "eee" }; //1.指针直接打印(无解引用) //1.1(个人认为这是最规范的方式) char* p = strArr[i]; //先得到单个字符串 printf("%s ", p); //在进行打印 //1.2 printf("%s\n", strArr[i]); //2.指针运算再打印(使用解引用) printf("%s ", *(strArr + i)); //ps.但这种方式我们可以打印某个字符串中的单个字符,如: printf("%c\n", *(*strArr + 1));
-
-
在打印时,如果要获取字符串数组中的一个字符串,一般先定义一个变量接收,再进行打印;如果要获取字符串数组中某一个字符串中的一个字符,既可以直接使用索引获得也可以使用二维数组的方式获得。
char strArr[5][100] = { "aaa", "abc", "ccc", "ddd", "eee" }; //获取字符串 char* str1 = strArr[1]; //方式一 printf("%s\n", str1); char* str2 = strArr + 1; //方式二(该方式存疑,会打印乱码) printf("%s\n", str2); printf("%s\n", *(strArr + 1)); //但如果不找变量接收而是直接打印是可以的 //获取单个字符(两种方式效果相同) char c = *(*strArr + 1) char c1 = strArr[1][1];
2. 常见函数
使用前记得添加<string.h>头文件。
- strlen:获取字符串的长度
- 单位为字节(注意中文占用两个字节);
- 不计算结束标记’\0’在内;
- strcat:拼接字符串
- 需要拼接的字符串可以被修改;
- 需要剩余空间足够容纳拼接内容;
- strcpy:由第二个字符串覆盖第一个字符串(第一个字符串多余的内容不会保留)
- 需要拼接的字符串可以被修改;
- 需要空间足够容纳拷贝内容;
- strcmp:字符串比较
- (内容和顺序)完全相同,得0;不相同,得非0;
- strlwr(_strlwr):将字符串变成小写
- strupr(_strupr):将字符串变成大写