前言
我的第一门语言就是C,但是学艺不精,中途跑去学了C#和Java后,感觉到了C的重要性,毕竟是最接近底层的语言,又跑回来学C。
毕竟前两门的控制语句,变量什么的都是类似的,回到C后只需要学习一些特定C的语法,比如宏,预编译指令等等,这些对我来说都是陌生的词汇。
所以边学边记录一下以前的知识。
提示:以下是本篇文章正文内容,下面案例可供参考
一、指针概念
以前在学校学的最后一课就是指针,可惜当时很菜听不懂,后面就结课了,也就没有去看了。想不到时隔几年,我又回头看这个了hhh。
● 先总结一点,此时有这样的一个指针p被变量A赋值了,如果打印下列的值:
int *p;
● &p就是变量A的地址。
● *p就是变量A的值。
● p也是变量A的地址(一开始就是一串地址)。
如果要准确的说,被赋值的时候就是一串地址,&就是变量在内存中的位置(一串16进制数),*就是变量在内存中的位置所指向的值(一个数值)。
如果理解不了就记得上面例子就行。
二、指针的基本使用
指针也就是内存地址,指针变量是用来存放内存地址的变量。就像其他变量或常量一样,您必须在使用指针存储其他变量地址之前,对其进行声明。指针变量声明的一般形式为:
type *var_name;
在这里,type 是指针的基类型,它必须是一个有效的 C 数据类型,var_name 是指针变量的名称。用来声明指针的星号 * 与乘法中使用的星号是相同的。但是,在这个语句中,星号是用来指定一个变量是指针。以下是有效的指针声明:
● int *ip; 一个整型的指针
● double *dp; 一个 double 型的指针
● float *fp; 一个浮点型的指针
● char *ch; 一个字符型的指针
内存地址都是一个代表内存地址的长的十六进制数。
#include <stdio.h> int main () { int var = 20; /* 实际变量的声明 */ int *ip; /* 指针变量的声明 */ ip = &var; /* 在指针变量中存储 var 的地址 */ printf("var 变量的地址: %p\n", &var ); /* 在指针变量中存储的地址 */ printf("ip 变量存储的地址: %p\n", ip ); /* 使用指针访问值 */ printf("*ip 变量的值: %d\n", *ip ); return 0; }
当上面的代码被编译和执行时,它会产生下列结果:
var 变量的地址: 0x7ffeeef168d8
ip 变量存储的地址: 0x7ffeeef168d8
*ip 变量的值: 20
三、指针地址的算数运算
可以对指针进行四种算术运算:++、- -、+、- 。
先普及一下不同类型的变量所占的字节数。
在64位编译器中:
● char :1个字节
● char*(即指针变量): 8个字节
● short int : 2个字节
● int: 4个字节
● unsigned int : 4个字节
● float: 4个字节
● double: 8个字节
● long: 8个字节
● long long: 8个字节
● unsigned long: 8个字节
接下来讲解一下指针的运算规则。
C 指针是一个用数值表示的地址。因此,您可以对指针执行算术运算。可以对指针进行四种算术运算:++、–、+、-。
假设 ptr 是一个指向地址 1000 的整型指针,是一个 32 位的整数,让我们对该指针执行下列的算术运算:
ptr++
在执行完上述的运算之后,ptr 将指向位置 1004,因为 ptr 每增加一次,它都将指向下一个整数位置,即当前位置往后移 4 字节(int)。这个运算会在不影响内存位置中实际值的情况下,移动指针到下一个内存位置。如果 ptr 指向一个地址为 1000 的字符,上面的运算会导致指针指向位置 1001,因为下一个字符位置是在 1001。
我们概括一下:
指针的每一次递增,它其实会指向下一个元素的存储单元。
指针的每一次递减,它都会指向前一个元素的存储单元。
指针在递增和递减时跳跃的字节数取决于指针所指向变量数据类型的字节长度,比如 int 就是 4 个字节。
#include <stdio.h> const int MAX = 3; int main () { int var[] = {10, 100, 200}; int i, *ptr; /* 指针中的数组地址 */ ptr = &var;//指向第0位的数组元素 for ( i = 0; i < MAX; i++) { printf("存储地址:var[%d] = %p\n", i, ptr ); printf("存储值:var[%d] = %d\n", i, *ptr ); /* 指向下一个数组元素 */ ptr++; } return 0; }
运行的结果如下:
可以看到,地址的内存地址是连续的。
但是,我代码不变,重新再运行一次:
怎么地址就给我改变了呢???
原来在C语言中,数组名被视为指向该数组的第一个元素的指针。因此,如果您有一个数组a,那么&a和a是等效的。
当您在程序中使用数组时,它实际上是在内存中连续分配的一段地址, 这些地址递增相等,并且每个地址都与数组中一个特定的元素相关联。
当您使用&运算符获取该数组的地址时,它将返回该数组的第一个元素的地址。这个地址是固定的,因为它指向类型为数组类型(例如int [])的数据块。
但是!!
当您对指针变量执行算术运算时(例如p ++或p + = 2),它将基于该指针当前指向的内存位置递增或递减其值。因此,在这种情况下,指针变量p所包含的值将发生更改,并且其地址也会随之更改。
总之,在C语言中,数组名是不可更改的常量表达式而且代表着固定位置上第一个元素所在内存地址,而指针变量则可以更改其包含值以反映不同位置处相关信息所存储内容所在内存地址。
四、指针数组的区别
可以定义用来存储指针的数组。
1)普通数组
#include <stdio.h> const int MAX = 3; int main () { int var[] = {10, 100, 200}; int i; for (i = 0; i < MAX; i++) { printf("Value of var[%d] = %d\n", i, var[i] ); } return 0; }
2)指向整数的指针数组
#include <stdio.h> const int MAX = 3; int main () { int var[] = {10, 100, 200}; int i, *ptr[MAX]; for ( i = 0; i < MAX; i++) { ptr[i] = &var[i]; /* 赋值为整数的地址 */ } for ( i = 0; i < MAX; i++) { printf("Value of var[%d] = %d\n", i, *ptr[i] ); } return 0; }
上述两个的结果都是
Value of var[0] = 10
Value of var[1] = 100
Value of var[2] = 200
3)指向字符的指针数组
#include <stdio.h> const int MAX = 4; int main () { const char *names[] = { "Zara Ali", "Hina Ali", "Nuha Ali", "Sara Ali", }; int i = 0; for ( i = 0; i < MAX; i++) { printf("Value of names[%d] = %s\n", i, names[i] ); } return 0; }
产生如下结果:
Value of names[0] = Zara Ali
Value of names[1] = Hina Ali
Value of names[2] = Nuha Ali
Value of names[3] = Sara Ali
五、双重指针
字面意思也就是指针的指针,C 允许指向指针的指针。
当一个指针指向一个变量的时候,那么这个指针就包含着这个变量在内存中的地址。
当一个二级指针指针上面的指针的时候,那么这个二级指针就包含着上面的指针在内存中的地址。
注意:指针也会占用内存地址。
下面声明了一个指向 int 类型指针的指针
int **var;
指针更像是一个找地址开门取物品的操作。其中*就是这个动作重复的次数,ptr 是取东西的门牌号也就是地址值:
● *ptr是完成一次开门取东西操作最终取出来的东西。
● **ptr是完成两次开门取物。需要注意的是第一次取得的是第二次要开的门的门牌号或者说地址,然后根据门牌号继续开门取物。 所以 *ptr 或者 **ptr 一定是取出来的东西,即为数值。而 ptr 一定是门牌号,即为地址值。
六、指针作为函数参数
通过引用或地址传递参数,使传递的参数在调用函数中被改变。
在C语言可以把指针或者数组作为参数传入函数的参数列表中。
1)指针作为形参
就相当于你得传一个地址进去。
定义头文件:void getSeconds(unsigned long *par);
在这个A函数中,你可以传入一个指针
定义一个变量: unsigned long sec;
调用函数并传参:getSeconds( &sec );
2)数组作为形参
定义头文件:double getAverage(int *arr, int size);
在这个B函数中,你可以传入一个数组
定义一个数组:int balance[5] = {1000, 2, 3, 17, 50};
调用函数并传参:getAverage( balance, 5 ) ;
七、指针作为函数返回值
C 语言不支持在调用函数时返回局部变量的地址,因为函数调用完,局部变量会被释放。
如果实在要返回局部变量地址,有两个方法。
● 返回局部static变量。
● 返回动态内存分配的地址,因为不会自动释放,需要我们手动释放。
int * myFunction() {//函数体}
注意:返回的地址,地址指向的内存的内容得存在,才有意义。
八、函数指针
顾名思义,函数指针就是一种指向函数的指针变量,在C语言中函数被编译后会被转换成内存地址,而函数指针就是用来存储这个地址的指针变量。
1)函数指针的格式
函数指针的格式如下:
返回类型 (*函数指针变量名)(参数列表)
其中,括号内是函数指针变量的定义,包括返回类型、函数指针变量名以及参数列表。需要注意的是,在参数列表中只需要列出数据类型,而不需要给出形参名。
例如,下面是一个接受两个int类型参数并返回int类型值的函数add:
int add(int a, int b) { return a + b; }
如果要定义一个指向add函数的函数指针变量p_add,则可以使用以下语法:
// 定义一个接受两个int类型参数并返回int类型值的函数指针 int (*p_add)(int, int); // 将p_add指向add函数 p_add = add;
这样就可以通过调用函数指针来实现动态调用不同的函数。
2)函数指针的实例
使用函数指针可以:
● 实现动态调用不同的函数
● 作为参数传递给其他函数以实现回调(重要)
等功能。在定义函数指针时需要注意其类型与定义的函数类型要匹配,即该函数指针变量只能存储与之匹配的特定类型的函数地址。
例如,下面是一个简单的加减法运算示例代码:
#include <stdio.h> // 定义一个接受两个int参数、返回int类型值的函数类型 typedef int (*fun_ptr)(int, int); // 定义一个加法运算的函数 int add(int a, int b) { return a + b; } // 定义一个减法运算的函数 int sub(int a, int b) { return a - b; } // 进行计算并打印结果 void calculate(int a, int b, fun_ptr func) { int res = func(a, b); printf("%d ", res); } int main() { // 声明两个整数 int a = 10, b = 5; // 定义一个类型为fun_ptr和名称为p_add的加法运算指针变量,并将其初始化为add函数地址 fun_ptr p_add = add; // 定义一个类型为fun_ptr和名称为p_sub的减法运算指针变量,并将其初始化为sub函数地址 fun_ptr p_sub = sub; // 使用函数指针进行加法计算并打印结果 calculate(a, b, p_add); // 使用函数指针进行减法计算并打印结果 calculate(a, b, p_sub); return 0; }
这段代码定义了两个不同的函数add和sub,分别代表加法和减法操作,然后定义了一个接受两个int类型参数、返回int类型值的函数类型fun_ptr,然后在函数main中定义了两个函数指针变量p_add和p_sub,并分别将其初始化为add和sub的地址。接着调用calculate函数,在其中使用不同的函数指针变量进行计算并打印结果。
九、回调函数
函数指针变量可以作为某个函数的参数来使用的,回调函数就是一个通过函数指针调用的函数。
简单讲:回调函数是由别人的函数执行时调用你实现的函数。
#include <stdlib.h> #include <stdio.h> void populate_array(int *array, size_t arraySize, int (*getNextValue)(void)) { for (size_t i=0; i<arraySize; i++) array[i] = getNextValue(); } // 获取随机值 int getNextRandomValue(void) { return rand(); } int main(void) { int myarray[10]; /* getNextRandomValue 不能加括号,否则无法编译,因为加上括号之后相当于传入此参数时传入了 int , 而不是函数指针*/ populate_array(myarray, 10, getNextRandomValue); for(int i = 0; i < 10; i++) { printf("%d ", myarray[i]); } printf("\n"); return 0; }