*第五章 指针和数组
- 指针变量(指针):是一种保存变量地址的变量
- 使用指针通常可以生成更高效,更紧凑的代码,但指针和goto语句一样,会导致程序难以理解,所以使用中尤为谨慎
5.1指针与地址
指针是能够存放一个地址的一组储存单元(通常情况下是2或4个字节),且指针占用内存的大小和类型无关,与编译器有关,32位的时候偶为4个字节,48位的时候为8个字节。
1. 指针的定义
指针的定义格式如下:
类型 *指针名;
int *p; // 定义一个指向整型的指针
2. 指针的初始化
指针可以通过取地址运算符 &
来初始化:
int a = 10;
int *p = &a; // p 现在指向变量 a 的地址
3. 指针的解引用
通过解引用运算符 *
,可以访问指针所指向的变量的值:
int value = *p; // value 现在是 10,即 a 的值
4. 修改指针所指向的值
可以通过指针修改所指向的变量的值:
*p = 20; // 现在 a 的值变为 20
eg:
int x =1, y=2 ,z[0];
int *ip; //ip 是指向int类型的指针
ip = &X ; // ip现在指向X 即将指针 ip 赋值为变量 x 的地址,即 ip 现在指向 x
y=*ip; // y的值现在为1 通过解引用指针 ip,将 x 的值(即 1)赋给变量 y,因此 y 的值现在是 1。
*ip =0; // X现在的值为0 将指针 ip 的值设置为 0,这实际上是将 x 的值设置为 0,因为 ip 仍然指向 x
ip=&z[0]; // 现在ip指向了z[0] 将指针 ip 赋值为数组 z 的第一个元素 z[0] 的地址,即 ip 现在指向 z[0]
这里有一个例外的情况(一个指向void类型的指针可以指向任何类型的指针,但它不能间接的引用自己)
5.2指针与函数参数
(指针的作用一:操作其他函数的变量)
参数传递方式
(由于本人已经学过java所以将C语言与JAVA进行了比较)
- C语言:
- C语言主要使用值传递(pass by value)。这意味着在调用函数时,实参的值会被复制到形参中,函数内部对形参的修改不会影响到实参。
- 通过指针(pass by reference)可以间接实现引用传递,允许函数修改实参的值。
- Java:
- Java的参数传递也是值传递,但对于对象类型的参数,传递的是对象引用的值。也就是说,函数内部可以通过引用修改对象的属性,但不能改变引用本身指向的对象。
- 对于基本数据类型,Java同样是值传递。
作用一:操作其他函数的变量
void swap(int *px ,int *py)
{
int temp;
temp =*px ;
*px =*py;
*py =temp;
}
细节:
函数中的变量的生命周期和函数有关,函数结束了,变量也会跟着小时,此时在其他的函数中,就无法通过指针去使用了;如果不想函数中的变量被回收,可以在变量的前边加static关键字。
5.3指针与数组
- 一般情况下使用指针写的程序比用数组下标编写的程序执行速度快。
- 在C语言中,pa[i]和*(pa+i)是等价的,简而言之,一个通过数组和下标实现的表达式可等价地的通过指针和偏移量实现
- 此外数组名和指针之间有一个不同之处,指针是一个变量,所以pa=a,pa++都是合法的,但是数组名不是变量,因此类似于a=pa和a++形式的语句是非法的
作用二:函数返回多个值
#include <stdio.h>
void getMaxAndMin(int arr[], int len, int* max, int* min) {
// 初始化最大值和最小值为数组的第一个元素
*max = arr[0];
*min = arr[0];
// 遍历数组,更新最大值和最小值
for (int i = 1; i < len; i++) {
if (arr[i] > *max) {
*max = arr[i];
}
if (arr[i] < *min) {
*min = arr[i];
}
}
}
int main() {
int arr[] = { 3, 5, 1, 8, 2 };
int max, min;
int len = sizeof(arr) / sizeof(arr[0]);
getMaxAndMin(arr, len, &max, &min);
printf("最大值: %d\n", max);
printf("最小值: %d\n", min);
return 0;
}
作用三 将计算结果和状态分开
#include <stdio.h>
// 函数声明
void divide(int dividend, int divisor, int *quotient, int *remainder);
int main() {
int a = 10;
int b = 3;
int result, remainder;
// 调用函数进行除法运算
divide(a, b, &result, &remainder);
// 输出结果
printf("商: %d, 余数: %d\\n", result, remainder);
return 0;
}
// 函数定义
void divide(int dividend, int divisor, int *quotient, int *remainder) {
if (divisor == 0) {
printf("错误:除数不能为零。\\n");
return;
}
*quotient = dividend / divisor; // 计算商
*remainder = dividend % divisor; // 计算余数
}
5.4地址算术运算
步长:指针移动一次的字节个数
char | short | int | long | long long |
---|---|---|---|---|
1字节 | 2字节 | 4字节 | 4字节 | 4字节 |
#include <stdio.h>
int main() {
int a = 10;
int* p = &a;
printf("%p\n", p);
printf("%p\n", p+1);
printf("%p\n", p+2);
return 0;
}
// 000000C2CB15F824
// 000000C2CB15F828
// 000000C2CB15F82C
有意义的操作
- 指针跟整数进行加减的操作(每次移动的步长)
- 指针跟指针进行减操作(间隔步长)
无意义的操作:
- 指针跟整数进行乘除操作
- 原因: 此时指针的指向不明
- 指针跟指针进行加乘除的操作
野指针 :指针指向的空间未分配
悬空指针:指针指向的空间已分配,但是被释放了
void类型的指针
- 优点
-
没有任何类型 可以接受任何类型指针记录的内容
2.缺点
-
无法获取变量里边的数据 也不可以计算
#include<stdio.h>
void swap(void* p1, void* p2, int len);
int main() {
//定义两个变量
int a = 10;
short b = 20;
//定义两个指针
int* p1 = &a;
short* p2 = &b;
printf("%d\n", *p1);
printf("%d\n", *p2);
void* p3 = p1;
void* p4 = p2;
long long c = 100L;
long long d = 200L;
swap(&c, &d, 8);
printf("c=%d,d=%d\n", c, d);
return 0;
}
// 函数用来交换两个变量记录的数据
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++;
}
}
这里关于C语言中的动态分配内存可以观看这个文章C语言的动态分布内存
5.5字符指针与函数
字符串常量是一个字符数组
在字符串的内部表示中,字符数组以空字符’\0’结尾,所以程序可以通过检查空字符找到字符数组的结尾,字符串常量占据的存储单元数也因此比双引号内的字符数大1
其下边两种定义就存在很大的差别
char amessage[]="now is the time" //定义一个数组
char *pmessage ="now is the time" // 定义一个指针
上述的声明中,anessage是一个仅仅存放初始化字符串以及空字符串’\0’的一维数组,其中数组的单个字符都可以进行修改。而pmessage是一个指针,它指向的是一个字符串常量,你可以让它指向不同的字符串常量,但是你不可以对其中的字符进行修改,其大概图示就是下边这样。
pmessage: [ ]----->[now is the \0]
amessgae: [now is the time\0]
C语言没有提供将整个字符串作为一个整体进行处理的运算符
void strcpy(char *s,char*t){
while (*s++=*t++)
;
}
- <string.h>中包含strcpy,strcmp等字符串处理函数
- *t++的值是指针t执行自增运算前t所指向的字符,后缀++表示在读取该字符之后才改变t的值;
进栈和出栈的标准用法:
*p++=val; //将val压入栈中
val=*--p; //将栈顶元素弹出到val中
5.6指针数组以及指向指针的指针(二级指针)
指针数组
char *lineptr[MAXLINES];
(例中用来存储待排序文本行的首字符地址),其中lineptr[i]是字符指针;又lineptr是一个数组名,指向其第一个元素*lineptr,该元素也是一个指针,指向第一个文本行的首字符;
二级指针
// 定义一级指针
int *p =&a;
// 定义二级指针
itn **pp=&p;
//利用二级指针修改一级指针记录的内存地址
*pp=&b
5.7多维数组
类似矩阵,但使用不如指针数组广泛,C语言中二维数组实际是一种特殊的一维数组,它的每个元素也是一个一维数组;按行存储
char daytab[2][13]={{...},{...}}; //定义一个二维数组,daytab[i][j]可引用元素
如果二维数组作为参数产地给函数,那么在函数声明中必须指定数组列数,行数无所谓:因为函数调用传递的是一个指针;以下三种都可以:
A. f(int daytab[2][13]){...}
B.f(int daytab[][13]){...} //一般说来,除数组的第一维(下标)可以不指定大小外,其余各维都必须明确指定大小;
C.f(int (*daytab)[13]){...} //因行数无关紧要,这种声明表明参数是一个指针,指向具有13个整型量的一维数组;若去掉括号:
int *daytab[13] //相当于声明了一个有13个整型指针元素的一维数组;
5.8指针数组的初始化
示例
// month_name函数 返回第n个月份的名字
char *month_name(int n)
{
static char *name[]={
"Illegal month","January","February","March","April","May","June","July","August","September","October","November","December"};
return(n <1 || n>12) ? name[0] :name[n]
}
name数组的初始化通过一个字符串序列表实现,列表中的每个字符串赋值给数组相应位置的元素。第i个字符串的所有字符储存在存储器的每个位置,指向他的指针储存在naem[i]中,由于上述声明中没有指明数组长度,因此编译器编译的时候将对处置个数进行统计,并将这一准确数字填入数组的长度。
5.9指针与多维数组
很容易混淆指针数组与二维数组之间的区别:
int a[10][20]; //分配了200个int长度的存储空间
int *b[10]; //仅仅分配了10个指针且没有初始化,每个指针都可以指向一个数组;
//具有重要优点:可以指向不同长度的数组(也可以不指向任何对象);
5.10命令行参数
C语言可以在程序开始执行时将命令行参数传递给程序;调用主函数main时它带有两个参数:
- argc(用于计数,表示运行程序时命令行中参数的数目);
- argv(用于参数向量,一个指向字符串数组的指针,每个字符串对应一个参数;通常用多级指针处理这些字符串);
- 约定:argv[0]的值是启动该程序的程序名,故argc至少是1;可选参数argv[1]~arf[argc-1];ANSI要求argv[argc]必须为一空指针。
5.11函数指针
格式:返回值类型 (*指针名)(形参列表)
#include<stdio.h>
void method1();
int method2(int num1, int num2);
int main() {
void (*p1)()=method1;
int (*p2)(int, int) = method2;
//利用函数指针调用函数
p1();
int num=p2(10,20);
printf("%d \n", num);
return 0;
}
void method1() {
ptintf("monthod1\n");
}
int method2(int num1, int num2) {
printf("method\n");
return num1 + num2;
}
函数本身不是变量,但是可以定义指向函数的指针;它可以被赋值、存放于数组、传递给函数以及作为函数的返回值等等;
排序程序通常包含3部分:判断两个对象次序的比较操作;颠倒对象次序的交换操作;用于比较和交换直到排序正确的算法。排序算法与比较和交换操作无关,所以在排序算法中调用不同的比较和交换函数,即可按照不同的标准排序。
5.12 复杂声明
C语言常因为声明(尤其函数指针)的语法问题收到批评;C的语法力图使声明和使用相一致;因为c的声明不能从左往右读并且使用了太多圆括号,所以情况较复杂时容易混淆 实际中很少使用复杂声明;使用typedef通过简单步骤合成,可以创建复杂声明。(剩下还有一部分讲解程序将声明转换为文字描述,以及相反过程,在笔记中暂没有写)