AutoLeaaders控制组——C语言指针学习笔记
一、地址
在计算机运行时,数据会存放在内存中,内存会以字节为单位划分为多个存储空间,并且为每个字节默认设置一个对应的编号,这个编号就是地址,为十六进制数。
地址只是计算机规定的一个值,所以不会占用内存的存储空间,地址显示的长度会根据系统及编译器的位数确定。64位编译器显示的地址为16个16进制数,32位编译器显示的地址为8个16进制数。
二、取地址符&
在C语言中,使用取地址符&可以获取内存中数据的地址。
同样的数据在每次运算时存放的地址可能会产生变化。
%p占位符指定输出的数据为地址,不可以用%x占位符代替。
例:
int main()
{
int a = 10;
printf("&a=%p", &a);
return 0;
}
000000689A5EF5C4就是变量a的地址
三、指针
指针 就是保存地址的变量。
普通变量 的值是实际的值
指针变量 的值是具有实际值的变量的地址。
1、声明指针
type *name ;
type:类型
name:指针变量名称
举例:
int main()
{
int a = 1;
int *pa;//*pa即为指针
pa = &a;
return 0;
}
这样就声明了一个指针*pa
同时声明两个指针应该 int *a, b;
而不是 int a, b;
2、解引用操作符 *
- *是一个单目运算符,用来访问指针的值所表示的地址上的变量
- 可以做左值,也可以做右值
- 例:int k = *p; *p = k + 1;
例子:
int main(){
int a = 1;
int* pa = &a;//pa存放a的地址
printf("&a = %p\n", &a);
printf("pa = %p\n", pa);
printf("a = %d\n", a);
printf("*pa = %d\n", *pa);
*pa = 2;
printf("a = %d\n", *pa);
return 0;
}
通过解引用符*,可以访问到指针中地址所指的变量,并对对应变量进行操作。
3、指针与函数
- 交换两个变量的值
void swap(int *pa, int *pb) {
int t = *pa;
*pa = *pb;
*pb = t;
}
int main() {
int a = 1, b = 2;
printf("a=%d, b=%d\n", a, b);
swap(&a, &b);
printf("a=%d, b=%d\n", a, b);
return 0;
}
通过指针,在函数里面可以访问到外面的变量,从而可以写出一个交换数值的函数
- 函数返回多个值,某些值就只能通过指针返回;
传入的参数实际上是需要保存带回的结果的变量;
void minmax(int a[], int len, int* min, int* max);
int main() {
int a[] = { 4,45,23,12,9,12,30,38 };
int min, max;
minmax(a, sizeof(a) / sizeof(a[0]), &min, &max);
printf("min = %d, max = %d\n", min, max);
return 0;
}
void minmax(int a[], int len, int* min, int* max)
{
*min = *max = a[0];
for (int i = 0; i < len; i++) {
if (a[i] < *min) {
*min = a[i];
}
if (a[i] > *max) {
*max = a[i];
}
}
}
通过指针,返回最大值与最小值两个值
- 函数返回运算的状态,结果通过指针返回;
常用的套路是让函数返回特殊的不属于有效范围内的值来表示出错;
-1或0(在文件操作会看到大量的例子);
但是当任何数值都是有效的可能结果时,就得分开返回;
int divide(int a, int b, int* result);
int main()
{
int a, b;
scanf("%d %d", &a, &b);
int c;
if (divide(a, b, &c)) {
printf("%d/%d=%d\n", a, b, c);
}
return 0;
}
int divide(int a, int b, int* result) {
int ret = 1;
if (b == 0) ret = 0;
else *result = a / b;
return ret;
}
分母不为0:
分母为0:
如果分母为0,那么返回值为0,判断为假,不输出结果;分母为0,返回值不为0,判断为真,则输出结果。
结果便通过指针存在变量c中返回;
4、指针与数组
例一:
void f(int a[]) {
printf("sizeof(a) = %d\n", sizeof(a));
printf("*a = %d\n", *a);
}
int main()
{
int a[] = { 1,2,3,4 };
printf("sizeof(a) = %d\n", sizeof(a));
f(a);
printf("sizeof(int*) = %d", sizeof(int*));
return 0;
}
由此可知,sizeof(a) = sizeof(;
函数参数表上的数组实际上是指针;
因此 void f(int a[]) 与 void f(int *a) 是等价的。
例二:
void f(int *a) {
printf("*a = %d\n", *a);
printf("a[0] = %d\n", a[0]);
printf("a[1] = %d\n", a[1]);
}
int main()
{
int a[] = { 1,2,3,4 };
f(a);
return 0;
}
由此可知,虽然传入函数的是一个指针,但是还是可以用数组的运算符[]进行计算,且传入的指针指向数组第一个数的地址。
数组变量是特殊的指针
- 数组变量本身表达地址,所以
- int a[10];int *p = a;//无需用&取地址
- 但是数组的单元表达的是变量,需要用&取地址
- a == &a[0]
例三:
int main()
{
int a = 10;
int* pa = &a;
printf("*pa=%d\n", *pa);
printf("pa[0]=%d\n", pa[0]);
return 0;
}
- []运算符可以对数组做,也可以对指针做
- 数组变量是const的指针,也就是 int b[] <==> int* const b,所以不能赋值。
5、指针与const
- 指针是const
- 表示一旦得到了某个变量的地址,不能再指向其他变量
int a = 10, b = 20;
int *const p = &a;//q是const
*p = 26;//OK
p = &b;//ERROR
- 所指是const
int a = 10, b = 20;
const int *p = &a;//(*p) 是const
p = &b;//OK
*p = 30;//ERROR
判断哪个被const了的标志是const在*的前面还是后面
- 转化
- 总是可以把一个非const的值转换成const的
void f(const int *x);
int a = 10;//a是非const
f(&a);//OK
const int b = a;//转化为const
f(&b);//OK
b = a + 1;//ERROR,因为b是const
- 当要传递的餐胡的类型比地址打的时候(如:结构体),这是常用的手段:既能用比较少的字节数传递值给参数,又能避免函数对外面的变量的修改
- const数组
- const int a[] = {1,2,3,4,5,6};
- 数组变量已经是const的指针了,这里的const表明数组的每个单元都是const int,所以必须通过初始化进行赋值。
- 保护数组值
- 因为把数组传入函数时传递的是地址,所以那个函数内部可以修改数组的值
- 为了保护数组不被函数破坏,可以设置参数为const
- 如:int sum(const int a[], int length);
6、指针运算
例一:
int main()
{
char c[] = { 0,1,2,3,4 };
char* pc = c;
printf("pc = %p\n", pc);
printf("pc + 1 = %p\n", pc + 1);
int i[] = { 0,1,2,3,4 };
int* pi = i;
printf("pi = %p\n", pi);
printf("pi + 1 = %p\n", pi + 1);
return 0;
}
为什么两种类型的指针加1,而char的地址加1,int的地址加4呢?
想到sizeof(char) = 1,sizeof(int) = 4;
由此可以得出 指针加1,地址加的是对应类型的大小
例二:
int main()
{
int a[] = { 1,2,3,4 };
int* p = a;
printf("a[0]=%d, *p=%d\n", a[0], *p);
printf("a[1]=%d, *(p+1)=%d\n", a[1], *(p + 1));
printf("a[2]=%d, *(p+2)=%d\n", a[2], *(p + 2));
return 0;
}
由上面结论可知
*p -> a[0] //*p指向a[0]
*(p + 1) -> a[1]
*(p + 2) -> a[2]
所以可以得出,给一个指针加i,表示要让指针指向下i个变量;同理,给一个指针减i,表示要让指针指向上i个变量
但,如果指针不是指向一片连续分配的空间(如:数组),则这种运算没有意义。
例三:
int main()
{
int a[] = { 1,2,3,4,5 };
int b[] = { 6,7,8,9,10 };
int* p = a, * q = b;
printf("p=%p\n", p);
printf("q=%p\n", q);
printf("q - p = %d\n", q - p);
return 0;
}
两个指针相减表示 指针所指的地址之间还能存放几个该类型的数
结论:
- 可以给指针加,减一个整数(+,+=,-,-=)
- 递增递减(++,–)
- 两个指针相减
“p++”:
- 取出p所指的那个数据来,完事之后顺便把p移到下一个位置去
- *的优先级虽然高,但是没有++高
- 常用于数组类的连续空间操作
- 在某些CPU上,这可以直接被翻译成一条汇编指令
指针比较:
- <, >, <=, >=, != 都可以对指针操作
- 可以比较它们在内存中的地址
- 数组中的单元的地址肯定是线性递增的
7、指针的类型
- 无论只想什么类型,所有的指针的大小都是一样的,因为都是地址
- 但是指向不同类型的指针时不能直接互相复制的
- 这是为了避免用错指针
8、指针的类型转化
- void* 表示不知道指向什么东西的指针
- 计算时与char*相同(但不相通)
- 指针也可以转化类型
int p = &i; void q = (void*)p; - 这并没有改变p所指的变量的类型,而是让后人用不同的眼光通过p看它所指的变量
四、动态内存分配
malloc函数
#include <stdlib.h>
声明:void* malloc(size_t size);
- 向malloc申请的空间的大小是以字节为单位的
- 返回的结果是void*,需要类型转化为自己需要的类型
- 格式:(int*)malloc(n*sizeof(int))
type* name = (type*)malloc(n*sizeof(type));
type:自己需要的指针类型
name:分配空间到的指针名
n:需要的空间数量
free函数
- 把申请的来的空间还给’系统‘
- 申请过的空间,最终都应该要还
- 只能还申请来的空间的首地址
例:
int main()
{
int* p;
int n;
scanf("%d", &n);
p = (int*)malloc(n * sizeof(int));
for (int i = 0; i < n; i++) {
p[i] = i;
printf("%d\n", p[i]);
}
free(p);
return 0;
}
常见问题
- 申请了没free——>长时间运行内存逐渐下降
- free过了再free
- 地址变过了,直接去free
五、注意:
野指针
概念:野指针就是指向的内存地址是未知的(随机的,不正确的,没有明确限制的)。
说明:指针变量也是变量,是变量就可以任意赋值。但是,任意数值赋值给指针变量没有意义,因为这样的指针就成了野指针,此指针指向的区域是未知(操作系统不允许操作此指针指向的内存区域)。
注:野指针不会直接引发错误,操作野指针指向的内存区域才会出问题。
int a = 10;
int *p;
*p = a; //无意义
p = 0x1145114; //无意义
*p = 20; //出错
因为*p并没有定义指向任何变量,因此p存的地址是随机的。
若再对其所指的内存空间赋值,那么会将20写在不该写的地方而出错。
解决方法:声明指针是先初始化,即 int *p = NULL;
0地址
- 当然你的内存中有0地址,但是0地址通常是个不能随便碰的地址
- 所以指针不应该具有0值
- 因此可以用0地址来表示特殊的事情:
- 返回的指针是无效的
- 指针没有被真正初始化(先初始化为0)
- NULL是一个预定定义的符号,表示0地址
- 有的编译器不愿意你用0来表示0地址