全面学习C语言【四】:指针(指针变量、指针运算符、指针数组、指针运算)、malloc动态内存分配

十、指针

使用&取地址

对于scanf 将输入的值传给一个变量 那么要加上&符号

scanf("%d",&i);

C语言的变量是放在内存中的 每个变量都有个地址 地址就是变量在内存中所存放的地方的位置
&符号 就能够获取到指定变量的地址 它是C语言中的取地址符

获取变量的地址 它的操作数必须是变量 而不能对没有地址的取地址

使用%p来输出地址:

int i=0;
printf("%p\n",&i);

地址的大小取决于编译器和系统是32位还是64位架构

相邻的变量的地址

同时定义的两个变量 它们在内存中的地址位置也是紧挨着的 是相邻的

int i1;
int i2;
printf("%p\n",&i1); // 0061FECC
printf("%p\n",&i2); // 0061FEC8

这是因为 在堆栈中 内存是自顶向下分配的 因此先定义的变量会在上面(即地址值更高)

数组的地址

数组的引用和数组的取地址是一样的 都是数组的地址
数组的地址使用的是数组中第一项(也就是下标为0)的地址
且数组中的每一项之间都是相邻的 紧贴着的

int i[10];
printf("%p\n",&i); // 0061FEA8
printf("%p\n",i); // 0061FEA8
printf("%p\n",&i[0]); // 0061FEA8
printf("%p\n",&i[1]); // 0061FEAC
printf("%p\n",&i[2]); // 0061FEB0

指针变量

将取得的变量的地址传递给一个函数 那么可以通过该地址在那个函数内访问这个变量

【指针】 是一种变量的类型
指针类型的变量就是专门用于保存地址的变量

下面是一个简单的例子:
将int类型的变量 i 的地址交给了指针p
int*中的*代表这是一个指针 int表示该指针指向的是int

int i;
int* p=&i;

p里保存的是变量i的地址 那么 可以说是p指向i

指针变量

星号*可以靠近类型 也可以靠近变量名
因此 并不是将星号加给了int这个类型 而是加给了距离星号最近的那个变量名

int* a,b; // 此时 a是指针类型 而b只是普通的int类型
int *a,b; // 此时 a是指针类型 而b只是普通的int类型

在C中并没有int*这个类型

指针变量的特点

在普通的变量中 存放的是实际的值
而在指针变量中 不会存放实际的值 存放的是具有实际的值的变量的地址

指针作为参数

在指针作为参数的时候 用void f(int *p);来定义
在调用的时候 要这么传入:

int i=123;
f(&i); // 传入的是int类型的变量的【引用】而不是原本的变量名或值

访问指针地址上的变量

若要访问 也很简单 只需要使用星号即可

当然 星号若作为双目运算符 那么是乘的意思
作为单目运算符 用于访问指针的值所表示的地址上的变量

#include <stdio.h>

void f(int *p);

int main()
{ 
	int i=123;
	printf("%p\n",&i); // 0061FECC
	f(&i);
	
	return 0;
}

void f(int *p)
{ 
	printf("%p\n",p); // 0061FECC
	printf("%d",*p); // 123
}

*n可以作为右值也可以作为左值
比如:

int i=*p;

*p=i+2;

可以直接通过指针来很方便地直接改变指针所指向的变量的值:

#include <stdio.h>

void f1(int *p);
void f2(int *p);

int main()
{ 
	int i=123;
	printf("%p\n",&i); // 0061FECC
	f1(&i);
	f2(&i);
	
	return 0;
}

void f1(int *p)
{ 
	printf("%p\n",p); // 0061FECC
	printf("%d\n",*p); // 123
	
	// 修改指针指向的位置的值 
	*p=321;
}

void f2(int *p)
{ 
	printf("%p\n",p); // 0061FECC
	printf("%d",*p); // 321
}

左值 之所以被称为左值 就是因为在赋值号=左边的并不是变量 而是一个实实在在的值 是表达式计算后的结果 是特殊的值

i[0]=123;
*p=123;

指针运算符的互相反作用

在指针中 用&获取地址 用*获取指定地址所代表的变量
它们是互相反作用的 即:

  • 若对一个获取的地址取所代表的变量 那就是原来的变量
  • 同样 若对一个指针所指向的变量取地址 那就是原来的地址

指针的应用场景

在使用指针之后 即可用其交换变量
由于函数的作用域范围问题 直接将值传入函数内 那么值在函数外 是不会被改变的
但若传入的是指针 那么即可改变指针所指向的地址上的值

#include <stdio.h>

void swap(int *pa,int *pb);

int main()
{ 
	int a=1;
	int b=2;
	swap(&a,&b);
	printf("%d\n",a); // 2
	printf("%d\n",b); // 1

	return 0;
}

void swap(int *pa,int *pb)
{ 
	int t=*pa;
	*pa=*pb;
	*pb=t;
}

因为函数只能有一个返回值 无法返回多个值 (而且也不能像Java一样返回一个对象… )
那么 当函数返回的是多个值 那么值只能通过指针返回

或者 遇到另一种情况 就是要分开返回结果
C中不像Java可以异常处理
在C中 只能函数返回(return)运算的状态 而函数的结果通过指针来返回

指针的注意点

需要注意的是 指针必须先指向一个变量的地址 然后才能被赋值
否则 指针里面什么都没有 此时可能默认指向的是内存中的一块未知区域 然后此时就被赋值了 那么可能会导致程序崩溃

指针和数组

在函数中通过参数接收到的外界的数组 其实只是数组的指针
在函数中接收到的数组的地址和外部的数组的地址是完全一模一样
但 可以用数组的运算符[]来对该数组进行运算或做其它操作
因此:

int f(int *arr);
其实等于
int f(int arr[]);

在C中 数组变量是特殊的指针 数组变量本身表达的就是地址
因此 对于数组 无需用&来取地址了(这是多此一举)

int arr[10];
int *p=arr; // 这样就行了

但数组的单元表达的是单个的变量 因此需要用&来取地址 因为里面存放的是确切的

数组变量名其实就是数组的地址 a=&a[0]

int a[]={ 1,2,3};

printf("%x\n",a); // 61feac
printf("%x\n",&a); // 61feac
printf("%x\n",&a[0]); // 61feac

[]运算符除了可以对数组使用 同样也可以对指针使用
对于指针变量 可以将一个普通变量当作是数组

int i=123;
int* p=i;
printf("%d\n",p); // 123
printf("%d",&p[0]); // 123

同样的 *运算符除了可以对指针使用 同样也可以对数组使用

int i[]={ 12,23,34};
printf("%d",*i); // 12

在C中 数组变量实际上是const(常量)的指针 因此不能被赋值

int a[]={ 1,2,3};
int b[]=a; // ×
int *p=a; // √
int b[] --> int * const b;

C99指针本身和指针所指向的那个值 都可以是const
情况一指针本身为const
一旦得到某个变量的地址 那么不能再指向其它变量 相当于是两者进行了绑定

int i=123;
const int *p=&i;
*p=321; // √
p++; // ×

情况二指向的值为const
无法通过指针去修改该变量(但并不意味着不能修改该变量)

int i=123;
int j=111;
const int *p=&i;
*p=321; // ×
i=321; // √
p=&j; // √

const在*的前面 则代表指针所指向的值不能被修改
const在*的后面 则代表该指针不能被修改

int i;
const int* p1=&1;
int const* p2=&i;
int *const p3=&i;

转换

可以将非const的值转换为const的值

void f(const int* i);

当要传递的参数的类型比地址还要大的时候 可用该方法 传递一个const的指针变量
如此 既能用较少的字节数来传递值给参数 同时又能避免函数外面对变量进行修改

const数组

数组变量已经const的指针了 再加const 代表数组中的每个单元都是const的int类型的值 不能再改变

const int arr[]={ 1,2,3,4,5};

因此 为保护数组不被函数修改 可设置该函数的参数为const

int f(const int arr[],int length);

指针的运算

当给指针+1的时候 实际上指针所增加的不一定是1
指针所增加的是sizeof(类型)的值

printf("%d\n",sizeof(char)); // 1
printf("%d\n",sizeof(int)); // 4

那么 char类型的指针+1 地址值增加的就是1
int类型的指针+1 地址值增加的就是4

由于数组中存放的值都是紧挨着的 且指针默认指向的是数组中的第一位 那么 指针 +1实际上就是让指针指向数组中的下一位

int arr[]={ 12,23,34,45,56};
int *p=arr;
printf("%p\n",p); // 0061FEB8 此时指向的是数组arr中的12这个数
printf("%p",p+1); // 0061FEBC 此时指向的是数组arr中的23这个数

同理 指针 -1实际上就是让指针指向数组中的上一位

printf("%p",p-1); // 0061FEB4

可以将指针看作是数组 也可以将数组看作是指针
那么 实际上 因为指针p默认是指向数组arr[0]
因此 当指针+1后 指针
(p+1)指向的是数组arr[1]
当指针+2后 指针*(p+1)指向的是数组arr[2] 以此类推

int arr[]={ 12,23,34,45,56};
int *p=arr;
printf("%p\n",p); // 0061FEB8
printf("%d\n",*p); // 12
printf("%p\n",p+1); // 0061FEBC
printf("%d\n",*(p+1)); // 23
printf("%p\n",p-1); // 0061FEB4
printf("%d\n",*(p-1)); // 4201696 下标越界后 取到的就是一串地址值
*(p+n) <==> arr[n]

注意

这一切的运算都是基于指针指向的是连续空间 例如数组
若指针指向的压根不是一片连续的空间 那又何来上一位下一位之说?
那就根本没有意义了

指针可以做哪些计算

  • 为指针加/减一个整数+ += - -=
  • 递增/递减++ --
    *p++ 即为 取出指针p所指向的位置的数据 在结束后将指针p指向的位置移到下一个 *p- -同理
  • 两个指针相减
    相减后的差 不是地址值的差 而是地址值的差/sizeof(类型) 即为它们中间相距几个该类型的距离

指针还可以进行比较

<
<=
==
>
>=
!=
这些 都可以对指针使用

当然 比较的并不是指针的值的大小 而是比较指针在内存中的地址的大小

0地址

在计算机的内存中 有个0地址 该地址无法读写
因此 0地址通常可以用于表达状态 例如:

  • 返回的指针无效
  • 指针未被正常初始化(指针先初始化为0)

但是 若用0来表示0地址 则有可能会崩溃
在C语言中 使用 NULL (全部大写)来表示0地址

指针的类型

void*来表示不知道指向什么类型的指针 (只知道有一个指针指向某个内存空间 仅此而已)

指向不同类型的指针无法互相赋值 因此需要避免用错了指针
当然 也可以进行强制类型转换

int i=123;
int *p=&i;
void *q=(void*)p;

上述代码的含义是将int类型的指针p强制类型转换为void类型的指针q 这并没有改变指针p指向的变量i的类型
若通过指针q看指针p 那么它就是void类型的
若通过指针p看 那么他依旧还是int类型的

指针的用途

  • 在需要传入较大的数据时 指针可以用作参数
  • 在函数传入数组后 可以用指针对数组进行操作
  • 当函数返回不止一个结果 可以用指针作为参数带出结果
  • 当需要用函数修改不止一个变量 可以传入指针来修改变量的值
  • 还可以动态申请内存

动态内存分配 / malloc&free

当遇到一种情况:预先不知道数组的大小 当输入的时候才知道
在这种情况下 C99可以用变量来定义数组的大小
但在C99之前 并不支持这么做 在这种情况下 就必须使用动态内存分配

使用 malloc 函数来指定要分配多少内存
(使用前需导入stdlib头文件#include <stdlib.h>)
malloc所要的参数是占据多少空间(以字节为单位)
malloc返回的是void*的类型 因此还需要强制类型转换
比如这样:

int n;
scanf("%d",&n);
int *a=(int *)malloc(n*sizeof(int)) // 分配一块用于存放int类型的内存空间

最后 在使用完后还需使用free函数将内存归还
free是和malloc相配套的函数 用于将申请来的空间还给计算机系统
在归还的时候 只能还申请来的空间的首地址 而不是地址多次改变后的值

free(a); // 归还内存空间

若不free的话 随着长时间运行 计算机的内存会逐渐下降 导致产生内存垃圾

==================== 例子 ====================
作为例子
可以用动态类型分配来实现自动控制数组大小:

#include <stdio.h>
#include <stdlib.h>

int main()
{ 
	int number;
	int* arr=0;
	int i;
	
	scanf("%d",&number);
	arr=(int *)malloc(number*sizeof(int));
	
	for (i=0;i<number;i++)
	{ 
		scanf("%d",&arr[i]);
	}
	
	for (i=number-1;i>=0;i--)
	{ 
		printf("%d\n",arr[i]);
	}
	
	// 动态内存分配 在用完之后 还需将内存归还
	free(arr);
	return 0; 
}

注意点:

在main函数的参数中 最好将参数定义成int argc,char const *argv[]

main(int argc,char const *argv[])
{ 
	...
}

argc是启动C程序时的参数个数 argv是启动C程序时的参数数组的指针 (这里的参数就是在运行C程序时在命令行输入的一串字符 用空格进行分隔)
(就像Java的main函数的String [] args)

    原文作者:Piconjo_Official
    原文地址: https://blog.csdn.net/Piconjo/article/details/106802905
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞