前言
本系列内容为程序媛学习C语言时做的笔记。以代码为主,并备注了打印结果以及详尽的解释注释。希望对你有所帮助。
C语言笔记入门篇包含多篇内容,当前位置:第二篇
注:本篇内容较第一篇过多
C语言并不可怕,请沉下心来,耐心就有收获。
多级指针
void pointerTest() {
int i = 100;
int *i_p = &i;//取出i的内存地址,赋值给i_p(一级指针)
int **i_pp = &i_p;//取出i_p的内存地址,赋值给i_pp(二级指针)
int ***i_ppp = &i_pp;//三级指针
int Q = *i_ppp;//取出三级指针(一个地址)对应的值,即:输出二级指针i_pp(&&i)
printf("i的值为:%d\n", i);
printf("&i的值为%p\n", i_p);
printf("&&i的值为%p\n", i_pp);
printf("&&&i的值为%p\n", i_ppp);
printf("*i_ppp的值为%p\n", Q);
/*
打印:
i的值为:100
&i的值为0061FEE4
&&i的值为0061FEE0
&&&i的值为0061FEDC
*i_ppp的值为0061FEE0
*/
}
指针类型 测试int/double
void pointStyle() {
int i = 1;
double d = 2;
int *i_p = &i;
double *d_p = &d;
printf("sizeof(i_p)大小为:%d\n", sizeof(i_p));//输出4
printf("sizeof(d_p)大小为:%d\n", sizeof(d_p));//输出4
printf("sizeof(i)小为:%d\n", sizeof(i));//输出4
printf("sizeof(d)大小为:%d\n", sizeof(d));//输出8
}
Q: 指针占用的内存大小是?
int double xxx 的指针 永远都是 4个字节(32位编译器)/ 4*2(64位编译器)
数组与数组指针
void arrTest() {
//int[] arrr = {3, 4};//这是错误的写法
int arr[] = {1, 2, 3, 4};
//测试arr地址
printf("打印arr:%p\n", arr);
printf("打印&arr:%p\n", &arr);
printf("打印&arr[0]:%p\n", &arr[0]);
/*
打印arr: 0061FEDC
打印&arr: 0061FEDC
打印&arr[0]: 0061FEDC
*/
//*arr=1
//arr是一个指针常量 不能修改 &arr是数组的地址 &arr[0] 是数组首元素地址 这三个相等 但是步长不一样
//sizeof(arr) = 16 , sizeof(&arr) = 4 , sizeof(&arr[0]) = 4
//步长:相邻两个元素之间开始地址的距离
// 数组 和 指针 挂钩 , 数组就是一个内存地址
// 数组的内存地址 == 第一个元素的内存地址 == &arr
//通过指针打印第三个元素
int *arrP = arr;//取出元素一的内存地址
arrP += 2;//挪到元素3的地址
printf("arr[3]的值:%d\n", *arrP);//取出元素三的内存地址的值
指针遍历arr
//for (int i = 0; i < 4; ++i) 这样写Linux上会报错
int i = 0;
for (i = 0; i < 4; ++i) {
printf("正常遍历取值 %d\n", arr[i]); //输出1234
printf("1:指针遍历取值 %d\n", *(arr + i)); //同上
printf("2:指针遍历取值 %d\n", *(&arr[0] + i)); //同上
printf("元素%d内存地址:%p\n", arr[i], arr + i); //打印如下:
}
//元素1内存地址:0061FED8
//元素2内存地址:0061FEDC
//元素3内存地址:0061FEE0
//元素4内存地址:0061FEE4
//析:数组是连续的内存空间(没有断层,有规律) 数组 每次挪动 4个字节 ==> int数组
//循环时给数组赋值
int *j_p = arr;
for (int j = 0; j < sizeof(arr) / sizeof(int); ++j) { //sizeof(arr)=16 sizeof(int)=4
*(j_p + j) = 1000 + j;
//赋值结果:1000 1001 1002 1003
}
printf("测试j_p[2]:%d\n", j_p[2]); //打印1002
}
函数指针
void add(int num1, int num2) {
printf("%d + %d = %d\n", num1, num2, num1 + num2);
}
void mins(int num1, int num2) {
printf("%d - %d = %d\n", num1, num2, num1 - num2);
}
void operate(void(*method)(int, int), int i1, int i2) {
method(i1, i2);
}
void methodPointer() {
void (*call)(int, int);
call = add;//方法赋值,写成“ call = &add ”也是一样的
//printf("%p, %p\n", add, &add);//add, &add值是一样的
operate(call, 1, 2);//输出:1 + 2 = 3
operate(mins, 3, 2);//输出:3 - 2 = 1
}
函数指针调用的几种写法:
//函数指针声明来接收函数
void test(void(*p)(int, int)) {
p(9, 9); //省略*
(*p)(9, 9);
(p)(9, 9); //函数的上面已经声明就是函数指针,所以可以省略*
}
以下高能预警
为更好的理解以上内容,作者查询了其它博客,并摘录了部分内容以方便读者查阅。
注:本文剩余部分引自其他作者发文,文末赋有原文地址,欲见详情可移步呦~
内存和地址:
(1)内存中每个位置由一个独一无二的地址标识;(2)内存中每个位置都包含一个值。
下面的例子显示了内存中的5个字的内容。
但是记住他们的地址太麻烦了,所以高级语言提供通过名字而不是地址来访问内存位置的功能,下面用名字代替地址:
这些名字我们称之为变量。名字和内存位置之间的关联并不是硬件提供的,而是编译器为我们实现的,硬件仍然通过地址访问内存位置。
指针的运算
C的指针的算数运算只包含以下两种形式:
(1)指针 +/- 整数
(2)指针 - 指针
第一种形式只能用于指向数组中的某个元素,和整数相加减就是让指针在数组中前后移动位置。值得注意的是,指针的移动是按数组中的类型决定的,假如数组类型是char类型,指针加一表示向后移动一个字节。而在int类型的数组中,指针加一是移动四个字节,并非一个,这个注意区分。
第二种形式的条件式两个指针都指向同一个数组,相减的结果是两个指针在内存中的距离。加入一个float类型的数组,每个类型占4个字节。如果数组的起始位置是1000,指针p1的值是1004,p2的值是1024。则p1-p2的结果是5。因为两个指针的差值(20)将除以每个元素的长度(4)。
对于指针的关系运算有:
< <= > >=
不过前提是它们都指向同一个数组中的元素。
指针和数组的区别/联系
int a;
a = 3;
编译器为了完成这两句代码,首先在编译过程中要创建一个符号表,样子大概如下图:
然后在运行过程中,编译器发现a=3这句代码时,会在符号表里找a对应的地址,然后把3放入对应的地址,即这里的0x1000。那如果是一个指针呢?即如果是*p=3会怎么做呢?首先,符号表变成了这个样子
在运行过程中,编译器遇到p=3时,首先要从符号表中找到p的地址0x1004,然后取出0x1004中的内容,这里假设为0x2000,最后把3放到0x2000内存地址中,即(0x2000)=3。
相信大家已经看明白了,想比于利用普通变量,利用指针存取数据的过程中多了一部取地址的的过程。这也就是指针变量于普通变量最大的不同。
下面再来看一下指针加偏移量的引用方式,还以上面的指针p为例,让我们来看一下*(p+2)=3的实现过程。首先,编译器从符号表中找到p然后,取出里面的内容0x2000,再根据其类型(int*),做一个运算,0x2000+2×sizeof(int)=0x2008。所以编译器会把3放入0x2008这个内存地址。整个过程可表示为*(*(0x1004)+2)=3。从这里也可以看出为什么指针必须有类型,因为在引用过程中要用到指针所指类型的长度。
最后来看一下,数组元素的引用是如何实现的,假设我们定义了一个数组,并对其元素进行了引用
int b[10];
b[4] = 3;
对应的符号表变成了这个样子
那b[4]=3如何完成呢?首先,找到符号b,然后发现其类型为int[](假想表达方式,C语言中不支持这样写),所以计算式变成了0x1008 + 4×sizeof(int)=0x1018,然后把3放入0x1018就可以了。用一个式子表达就是*(0x1008+4)=3。
从上面的寻址式子可以看出,普通变量、指针、数组三者对于编译器的区别。具体到数组,它即具有普通变量的直接性,即不用取两次地址里的内容而是取一次,同时又具有和指针相同的偏移量引用方式,即下标的实现实际是由指针加偏移量实现的。
为了表明上述事实(或者是为了提高C语言入门门槛),C语言对指针与数组的引用方式做了可以“交叉”使用的语法规定。就上面的例子来说,如果p指针指向数组b时,b[i]、(b+i)、p[i]、(p+i)都是对数组第i个元素的正确引用方式,这也给很多C语言学习者制造了“指针和数组一样”的错觉。
在函数形参中的表现
在向函数传递参数时,如果实参是一个一维数组,那用于接受的形参为对应的指针。也就是传递过去是数组的首地址而不是整个数组。这么做的原因主要是效率,这是无可争议的,但为了使用上的简便与通用,C语言接受两种形参的写法,即下面两种写法是相同的
int foo(int *a, int n);
int foo(int a[], int n);
甚至是
int foo(int a[20], int n);
在底层都是完全一样的形式。这简化了C语言的使用,但同时也增加了对其进一步理解的难度。正因为很多程序稀里糊涂就通过了,所以程序员就会一直稀里糊涂下去。至于哪种写法好的争论,只要你理解了,用哪种写法到是不必强求。指针形式表明了传参过程的实质,而数组形式表明了这个指针对应一个数组实参,甚至后面的数字可以提供数组长度的参考。
摘录部分出处
本文后半部分引自以下两篇文章,在此感谢两位巨人的肩膀。
https://blog.csdn.net/cyfcsd/article/details/54773355
https://www.cnblogs.com/maluning/p/7955648.html