文章目录
数组、指针和函数
0、前言
之前大一的时候,学习C语言,学到数组的时候感觉还行,可以接受。但是学到指针和函数后,绕来绕去的,就开始懵圈了。
但是,指针的重要性不言而喻,检验C语言学得好不好,就看“指针和内存管理”了。
大一下之后呢,就一直学Java,虽说原理都是相近的,但是毕竟C的语法还是比较细的,而且最近因为一些原因呢,要用C语言实现算法,所以抽空补补了C的基础和语法,特此记录一下。
1、数组的内存中的存储
那些简单的数组定义、初始化等比较简单就不说明,我们重点来看数组在内存中的存储。
在此,我们先来补充一下内存地址原理,其实这个知识点是《计算机组成原理》的内容,在此简单提一下,顺带补充一句,基础很重要,小伙伴们要重视基础哦!
假设我们的机子32位,则会使用4字节来存储地址,例如”0x11223344”;若为64位,则用8字节来存储地址,例如“0x1122334455667788”。
以32位机为例:
接着,我们先写一段很简单的程序,然后我们再来看,VS2022调试中的内存映像。
#include<stdio.h>
int main() {
int arr[5] = { 11,22,33,44,55 };
for (int i = 0; i < 5; i++) {
printf("%d\n", arr[i]);
}
return 0;
}
内存映像:
通过内存映像我们可以很清楚的看到,例如:
- 一个int类型确实占4字节
- 数组在内存中是顺序存放的,大小根据元素的类型和个数决定
- 数组名就是数组的首地址,也就是数组第一个元素的地址
2、指针
2.1、指针和指针变量
指针:一个变量的地址称为该变量的指针,也就是说,指针就是变量的地址。
指针变量:用来存放为“地址”的变量。
int num = 18;
int *point = #
假设32位机,num存放的地址为“0x 11 22 33 44”。
我们就说,变量num的地址(指针)是“0x 11 22 33 44”;指针变量point的值是“0x 11 22 33 44”。
我们都知道int类型的变量num占4个字节(32位机),那么指针变量point占多大的空间呢?答若为32位机,寻址范围为32位即4字节;若为64位机,则寻址范围为64位即8字节。
2.2、操作符
-
取地址操作符为“&”,也称“引用”,通过该操作符我们可以获取一个变量的地址值;
-
取值操作符为“*”,也称“解引用”,通过该操作符我们可以得到一个地址对应的数据。
#include<stdio.h>
int main() {
int num = 15;
int* point = #
printf("num: %d\n", num);
//若编译器不支持%p,可以用%u、%lu来代替
printf("&num: %p\n",&num);
printf("point: %p\n",point);
printf("*point: %d\n",*point);
return 0;
}
/*
结果:
num: 15
&num: 00EFFCBC
point: 00EFFCBC
*point: 15
*/
2.3、指针操作
2.3.1、指针和整数相加减
我们以加法为例,减法同理。
#include<stdio.h>
int main() {
int arr[5] = { 1,2,3,4,5 };
int* point = arr;
printf("%p\n", point);
printf("%p\n", point+2);
printf("%d\n", *point);
printf("%d\n", *(point+2));
return 0;
}
/*
结果:
00DDFD00
00DDFD08
1
3
*/
0x 00 DD FD 00 -- 0x 00 DD FD 08,一共是相差8个字节,即两个int类型变量所占的空间
用"+"运算符把指针和整数相加(相减),整数都会和指针所指向类型的大小相乘(以字节为单位),再将结果与初始地址相加。
即,0x00DDFD00 + 2 * 4Byte = 00DDFD08
point + 2后的值就是arr[2]的地址,即(point + 2)与&arr[2]等价
2.3.2、指针递增
#include<stdio.h>
int main() {
int arr[5] = { 1,2,3,4,5 };
int* point = &arr[2];
printf("%p\n", point);
printf("%d\n", *point);
printf("%p\n", ++point);
printf("%d\n", *point);
return 0;
}
/*
结果:
00F8FE78
3
00F8FE7C
4
*/
0x 00 F8 FE 78 --- 0x 00 F8 FE 7C,一共相差4个字节
一开始point指向arr[2],其值为"00F8FE78";而后point递增后指向arr[3],其值为"00F8FE7C"
所以,point++相当于把point的值加上4,即 加上(1 * 4 Byte)
2.3.3、指针求差
#include<stdio.h>
int main() {
int arr[5] = { 1,2,3,4,5 };
int* point01 = &arr[2];
int* point02 = &arr[5];
printf("%p\n", point01);
printf("%p\n", point02);
printf("%d\n", point02 - point01);
return 0;
}
/*
结果:
006FFB38
006FFB44
3
*/
0x 00 6F FB 38 --- 0x 00 6F FB 44,一共相差12个字节
point02 - point01 得3,意思是两个指针所指向得两个元素之间相差3个int,即12个字节
2.3.4、& 和 * 同时使用
//若有此语句
int a = 18;
int* point = &a;
&*point 和 *&a 的含义和区别
“&”和“*”两个运算符的优先级别相同,但要按自右向左的方向结合。
因此,&* point 与 &a 相同,都表示变量a的地址,也就是point。
而*&a,首先进行&a运算,得到a的地址,再进行*运算。*&a和*point的作用是一样的,它们都等价于变量a,即*&a 与a等价。
2.4、指针类型与地址
地址应该和指针类型兼容,也就是说,我们不能把int型变量的地址赋值给指向double类型的指针,只能将int型变量的地址赋值给指向int类型的指针变量中。
用代码表示如下:
int num01 = 18;
double num02 = 3.14;
int* point01 = &num01; //正确,歪瑞good
double* point02 = &num01; //毫无意义而且会出错
double* poinr03 = &num02; //正确,歪瑞good
我们在开发过程中,应该避免出现这种问题。
但是,如果硬要这么做的话,又有什么影响呢?
其实是可以赋值的,但是会出现数据截断等错误,而且不能进行指针操作,由于篇幅所限,详情请看《C语言_地址与指针类型不兼容造成的影响》。
3、函数
3.1、全局变量和局部变量
#include<stdio.h>
int i = 18; // 全局变量
void print(int a) {
printf("print -- &a = %p\n", &a);
printf("print -- a = %d\n", a);
}
int main() {
printf("main -- &i = %p\n", &i);
printf("main -- i = %d\n", i);
print(i);
int i = 5; //局部变量
printf("main -- &i = %p\n", &i);
printf("main -- i = %d\n", i);
print(i);
return 0;
}
全局变量i存储在数据段,所以main函数和my_print函数都是可见的。
全局变量不会因为某个函数执行结束而消失,在整个进程的执行过程中始终有效,因此开发中应尽量避免使用全局变量!
我们在函数内定义的变量都称为局部变量,局部变量存储在自己的函数对应的栈空间内,函数执行结束后,函数内的局部变量所分配的空间将会得到释放。
如果局部变量与全局变量重名,那么将采取就近原则,即实际获取和修改的值是局部变量的值。
3.2、堆空间和栈空间的差异
我们用一个例子来看一看栈空间和堆空间的差异。
#include <stdio.h>
char* print_stack()
{
char c[17] = "I am print_stack";
puts(c);//能正常打印
return c;
}
char* print_malloc()
{
//malloc是在堆空间中开辟一块内存
char* p = (char*)malloc(30);
strcpy(p, "I am print_malloc");
puts(p);
return p;
}
int main()
{
char* p;
p = print_stack();//栈空间会随着函数的执行结束而释放,相当于p=c
puts(p);//打印不出来
p = print_malloc();//堆空间不会随子函数的结束而释放,必须自己free
puts(p);
free(p);
return 0;
}
4、函数、数组和指针
假设我们有一个需求,是要对数组进行求和,我们可以怎样定义函数呢?
#include<stdio.h>
#define NUM 10
int getSum01(int arr[], int num);
int getSum02(int* arr, int num);
int getSum03(int* start, int* end);
int main() {
int arr[NUM] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int* point = arr;
int result01 = getSum01(arr, NUM);
int result02 = getSum02(point, NUM);
int result03 = getSum03(point, point + NUM);
printf("result01 = %d\n", result01);
printf("result02 = %d\n", result02);
printf("result03 = %d\n", result03);
return 0;
}
int getSum01(int arr[], int num) {
printf("getSum01:arr = %p\n", arr);
int result = 0;
for (int i = 0; i < num; i++) {
result += arr[i];
}
return result;
}
int getSum02(int* arr, int num) {
printf("getSum02:arr = %p\n", arr);
int result = 0;
for (int i = 0; i < num; i++) {
result += *(arr + i);
}
return result;
}
int getSum03(int* start, int* end) {
printf("getSum03:start = %p\n", start);
printf("getSum03:end = %p\n", end);
int result = 0;
while (start < end) {
result += *start;
/*
* 让指针指向下一个元素
* 指针++并不是指向下一个字节的地址;
* 而是指向下一个元素的地址,增量依据类型而定
*/
start++;
}
return result;
}
/*
结果:
getSum01:arr = 006FFB30
getSum02:arr = 006FFB30
getSum03:start = 006FFB30
getSum03:end = 006FFB58
result01 = 55
result02 = 55
result03 = 55
*/
int getSum01(int arr[], int num);
int getSum02(int* arr, int num);
int getSum03(int* start, int* end);
从输出的结果可以出,以上三个函数是等价的!
之前,我们也说过,数组名就是数组首元素地址。
无论是将函数形参定义成“数组”亦或是“指针”,我们传入的“数组名”亦或是“指针”,其实在底层最后传的都是“指针”,也就是“地址”,对应到本例就是“数组首元素的地址”。
这样也就能明白,为什么我们定义函数时需要两个形式参数,即“数组地址”和“数组大小”。
因为在函数实现中,我们并不知道数组的长度,盲目运算可能会造成空指针异常,因此我们要手动地传入“数组长度”。当然,我们也可以传入“数组首地址”和“数组尾地址”,其实原理都是一样的。
emmm,大概就唠这么多吧,最近发现基础真的重要!!!
注:如有错误,敬请指正!!!