C语言的指针
指针
1. 内存
1.1 内存的作用
- 暂存放CPU中的运算数据
- 暂存与硬盘等外部存储器交换的数据
1.2 物理存储器和存储地址空间
物理存储器:
实际存在的具体存储器芯片
- 主板上装插的内存条
- 显示卡上的显示RAM芯片
- 各种适配卡上的RAM芯片和ROM芯片
存储地址空间:
对存储器编码的范围
- 编码:对每个物理存储单元(一个字节)分配一个号码
- 寻址:可以根据分配的号码找到相应的存储单元,完成数据的读写
1.3 内存地址
-
将内存抽象成一个很大的一维字符数组
-
编码就是对内存的每一个字节分配一个32位或64位的编号(与32位或者64位处理器相关),这个内存编号我们称之为内存地址
-
内存中的每一个数据都会分配相应的地址
-
char
:占一个字节分配一个地址 -
int
: 占四个字节分配四个地址 -
float、struct、函数、数组等
1.4 指针和指针变量
-
内存区的每一个字节都有一个编号,这就是“地址”
-
如果在程序中定义了一个变量,在对程序进行编译或运行时,系统就会给这个变量分配内存单元,并确定它的内存地址(编号);
-
指针的实质就是内存“地址”。指针就是地址,地址就是指针
-
指针是内存单元的编号,指针变量是存放地址的变量;
-
通常我们叙述时会把指针变量简称为指针,实际他们含义并不一样;
-
指针变量的大小为4个字节,每个字节都有地址编号;64位操作系统指针变量为8个字节,范围是0x0000000000000000 -0xffffffffffffffff;
2. 指针的定义与使用
-
指针也是一种数据类型,指针变量也是一种变量
-
指针变量指向谁,就把谁的地址赋值给指针变量
-
*操作符操作的是指针变量指向的内存空间
2.1 指针的声明
指针变量声明的一般形式为:
type *var-name;
- type 是指针的基类型,它必须是一个有效的C数据类型;
- var-name 是指针变量的名称
有效的指针声明:
int *ip; /* 一个整型的指针 */
double *dp; /* 一个 double 型的指针 */
float *fp; /* 一个浮点型的指针 */
char *ch /* 一个字符型的指针 */
注意:
&可以取得一个变量在内存中的地址。但是,不能取寄存器变量,因为寄存器变量不在内存里,而在CPU里面,所以是没有地址的
2.2 指针的使用
- 声明一个实际变量
- 声明一个指针变量
- 在指针变量中存储实际变量的地址
例如:
int var = 1;
int *ptr;
ptr = &var;
*ptr代表取地址所代表的空间的内容
示例:
// STATEMENT : 指针的创建和使用
#include <stdio.h>
void testPointer() {
int a = 10;
int *pInt = &a;
printf("pInt=%p\n", pInt); //pInt=000000000061FDE4
printf("pInt=%d\n", *pInt); //pInt=10
//根据指针修改a的值
*pInt = 12;
printf("a = %d", a); //a = 12
}
int main() {
testPointer();
return 0;
}
2.3 指针大小
- 使用
sizeof()
测量指针的大小,得到的总是:4或8 - sizeof()测的是指针变量指向存储地址的大小
- 在32位平台,所有的指针(地址)都是32位(4字节)
- 在64位平台,所有的指针(地址)都是64位(8字节)
- // STATEMENT : 指针的大小
#include <stdio.h>
int main() {
int a = 1;
float b = 2.1f;
double c = 3.14;
char e = 'w';
int *p1 = &a;
float *p2 = &b;
double *p3 = &c;
char *p4 = &e;
printf("%llu\n", sizeof(p1));//8
printf("%llu\n", sizeof(p2));//8
printf("%llu\n", sizeof(p3));//8
printf("%llu\n", sizeof(p4));//8
}
2.4 指针的宽度(步长)
指针的宽度 = sizeof(将指针变量与指针变量最近的*拖黑,剩下的类型)
宽度也叫做步长;
步长: 指针加1跨过多少个字节;
char *p 1
short *p 2
int *p 4
Int **p sizeof(int *) 4
示例:
// STATEMENT : 指针的步长
#include <stdio.h>
int main() {
int num = 0x01020304;
char *p1 = (char *) #//int *
short *p2 = (short *) #
int *p3 = #
int **p4 = &p3;
//通过*取指针变量所指向那块空间内容时,取的内存的宽度和指针变量本身的类型有关
printf("%x\n", *p1);//04
printf("%x\n", *p2);//0304
printf("%x\n", *p3);//01020304
printf("%lld\n", sizeof(*p1)); //1
printf("%lld\n", sizeof(*p2)); //2
printf("%lld\n", sizeof(*p3));//4
printf("%lld\n", sizeof(*p4));//8
return 0;
}
2.5 野指针和空指针
野指针
定义: 野指针就是没有初始化的指针,指针的指向是随机的
例:
int *p;
*p = 200;
//p 没有初始化,p就是一个野指针
空指针
定义: 将指针的赋值为NULL;
作用: 如果使用完指针,将指针赋值为NULL;这样的话,在使用时判断指针的值是否为NULL,就可以知道指针是否使用过了;
空指针示例:
// STATEMENT : 空指针
#include <stdio.h>
int main() {
int a;
//将指针的值赋值为0,0x0000000 = NULL
int *p = NULL;//给指针p的内容赋值为0
*p = 200;//err 因为p保存了0x0000的地址,这个地址是不可以使用的,非法
printf("%d\n", *p);
return 0;
}
2.7 万能指针void *
void *
指针可以指向任意变量的内存空间
注意:
- 不可以定义void类型变量,因为编译器不知道分配多大的空间;
- 可以定义void *类型,因为指针都是4或者8字节;
万能指针示例:
// STATEMENT : 万能指针 void*
#include <stdio.h>
int main() {
int n1 = 5;
double n2 = 3.2;
void *p1 = &n1;
void *p2 = &n2;
//用万能指针获取变量的值
/*printf("n1=%d\n", *p1);*/
//不能使用万能指针去获取内存空间中的值,因为void,类型编译器不知道要分配多少空间
printf("n1=%d\n", *(int *) p1); //n1=5
printf("n1=%f\n", *(double *) p2); //n1=3.200000
//先把指针类型由void*转换成其他类型,再获取内存中数值;
return 0;
}
2.8 const修饰的指针变量
首先,回顾一下const修饰的普通变量,
假设:
const int n = 10;
这说明,我们不能直接通过变量n去修改它的值;但是,却可以通过地址去间接修改变量n的值;
const int *p = &n;
代表不能通过*p去改变p指向空间中的内容
int * const p = &n;
说明p的指向不能被改变;
const int *const p = &n
说明p的指向不能改变而且不能通过*p去直接修改p所指向内存空间的内容;
void testConstPtr() {
int a = 10;
//const int *p = &a;
int *const p = &a;
*p = 14;
printf("a=%d", a); //a=14
}
3. 多级指针
二级指针就是指向一个一级指针变量地址的指针
二级指针使用实例:
// STATEMENT : 多级指针
#include <stdio.h>
int main() {
double a = 3.14;
//一级指针
double *p = &a;
//二级指针
double **pp = &p;
//根据二级指针修改a的值
**pp = 6.28;
printf("a=%f", a); //a=6.280000
return 0;
}
4. 指针与数组
4.1 数组名
数组名字是数组的首元素地址,但它是一个常量,不能被修改;
4.2 使用指针操作数组元素
// STATEMENT : 使用指针实现数组的遍历操作
#include <stdio.h>
int main() {
int a[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
//让p指向数组首元素地址
int *p = a;
int length = sizeof(a) / sizeof(a[0]);
for (int i = 0; i < length; i++) {
printf("%d ", *p); //1 2 3 4 5 6 7 8 9
p++;//每次跨过一个步长的单位
}
return 0;
}
/**
* 通过指针给数组元素赋值
*/
void assignmentArr() {
int a[8] = {0};
int *pInt = a;
for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++) {
//通过指针给元素赋值
*pInt = i * 2;
pInt++;
}
//遍历数组
for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++) {
printf("%d ", a[i]); //0 2 4 6 8 10 12 14
}
}
- 指针+1,跨过一个步长;
- int *p; step = sizeof(int);
5. 指针加减运算
- 量指针(类型一致)相减,得到的是中间跨过了多少元素;
- 两指针相减没有意义(vs里面编译可以通过,但是cLion中编译不通过)
;
示例:
void calcOne() {
int arr[] = {2, 3, 4, 5, 6, 7};
int *p1 = arr;
//定义指向末尾元素的指针
int *p2 = (int *) (&arr + 1) - 1;
//计算跨过多少元素
int length = p2 - p1;
printf("length = %d", length);//length = 5
}
void calcTwo() {
int arr[] = {2, 3, 4, 5, 6, 7};
int *p1 = &arr[0];
int *p2 = &arr[4];
/* int num = p2 + p1;*/ //指针不能相加,编译器不通过;
}
补充:
[]==*()
,即[]并不是数组特有的符号p[3]==*(p+3)
int main(){
//[] == *()
int a[10] = { 1,2,3,4,5,6,7,8,9,10 };
int *p = a;
for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++){
//printf("%d ",a[i]);//a[i] == *(a+i)
//printf("%d ", *(p+i));
//printf("%d ", p[i]);// p[i] == *(p+i)
printf("%d ", *(a + i));
}
return 0;
}
6. 指针数组
定义:由指针元素组成的数组;
6.1 指针数组的创建
语法:
type *name[n] = {&a,&b...}
- type * :指针的类型
- name :数组名
- n :元素个数
- {} :具体的元素
示例1:
// STATEMENT : 指针数组
#include <stdio.h>
void initPointerArray() {
double a = 1.21;
double b = 2.24;
double c = 3.14;
double *arr[3] = {&a, &b, &c};
//遍历指针数组,获取对应空间中存放的数据;
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) {
printf("%.3f ", *arr[i]); //1.210 2.240 3.140
}
}
定义指针指向指针数组首地址:
void test2() {
double a = 1.21;
double b = 2.24;
double c = 3.14;
double *arr[3] = {&a, &b, &c};
double **k = arr;
//获取首元素的值
printf("arr[0] = %.2f\n", **k); //arr[0] = 1.21
//通过k这个二级指针,遍历该数组;
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) {
// printf("%.2f ", *(double *) k[i]); //1.21 2.24 3.14
printf("%.2f ", **(k + i)); //1.21 2.24 3.14
}
}
7. 指针和函数
7.1 函数形参改变实参的值
#include <stdio.h>
/**
* 交换两个变量的值
* @param x
* @param y
*/
void swapValue(int *x, int *y) {
int temp = *x;
*x = *y;
*y = temp;
}
int main() {
int x = 1;
int y = 2;
swapValue(&x, &y);
printf("x=%d\n", x); //x=2
printf("y=%d\n", y); //y=1
return 0;
}
7.2 数组名做函数参数
把数组名作为参数时出现的问题
代码如下:
#include <stdio.h>
void printArr(int a[6]) {
for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++) {
printf("%d ", a[i]);//1 2
}
}
int main() {
int arr[6] = {1, 2, 3, 4, 5, 6};
printArr(arr);
return 0;
}
原本有6个元素的数组,只打印出了1,2这两个值;
原因:
- 当把数组名作为参数传递到
printArr
中去时,实际上是传递的数组的首地址,而数组作为函数形参会退化为指针,因此sizeof(a)==8; - 这里的sizeof(a[0])等价于
sizeof(*a[0]) -> sizeof(*(a+0)) -> sizeof(*a) -> sizeof(1) == 4
; - 因此,
sizeof(a) / sizeof(a[0]) == 2
;所以条件为i<2;因此只打印出了两项内容;
注意:
- 数组作为函数形参会退化为指针
- 如果想把数组作为参数传递,则需要传递一个指针类型的形参和一个int的形参(代表数组长度);
解决方法
定义一个指针类型的变量当做形参,接收数组的首地址;但是这样的话无法知道传递过去的数组的长度,因此还需要一个int类型的参数,专门用来接收数组的长度;
代码示例:
#include <stdio.h>
void printValue(int *array, int length) {
for (int i = 0; i < length; i++) {
printf("%d ", *(array + i)); //1 2 3 4 5 6
}
}
int main() {
int arr[6] = {1, 2, 3, 4, 5, 6};
printValue(arr, sizeof(arr) / sizeof(arr[0]));
return 0;
}
7.3 指针做为函数的返回值
// STATEMENT : 指针作为返回值
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
int num = 0;//在函数外面定义的变量叫全局变量,整个工程都可以使用
//整个变量程序启动开辟空间,直到程序结束释放空间
int *getNum() {
//{}中定义的变量叫局部变量,局部变量在函数结束之后的空间会被释放
srand(time(NULL));
num = rand();
return #
}
int main() {
int *p = getNum();
printf("%d\n", *p); //10048
return 0;
}
8. 指针和字符串
8.1 字符指针
// STATEMENT : 字符数组和指针
#include <stdio.h>
#include <string.h>
/**
* 通过指针遍历字符数组
*/
void charPointer() {
char ch[] = "whatrudoing";
char *c = ch;
for (int i = 0; i < sizeof(ch) / sizeof(ch[0]); i++) {
printf("%c ", *(c + i));
}
printf("\n");
printf("%s\n", c); //%s打印字符串,要的是首元素的地址
printf("%s\n", c + 2); //atrudoing; 表示获取从首元素移动两个元素之后的字符串
printf("%c\n", *(c + 3));//t ; 获取第四个字符
printf("%llu\n", sizeof(ch));//12(字符串长度+!)
printf("%llu\n", sizeof(c));//8(64位地址的长度)
// sizeof(ch) != sizeof(c);
printf("%llu\n", strlen(ch));//11
printf("%llu\n", strlen(c));//11
//strlen(ch) == strlen(c);
}
8.2 字符指针做函数参数
// STATEMENT : 字符指针作为函数参数
#include <stdio.h>
#include <string.h>
/**
* @method:合并两个字符串
* @param src :目标字符串
* @param dest:要合并的字符串
*/
void concatString(char *src, char *dest) {
int len = strlen(src);
int i = 0;
for (i = 0; i < strlen(dest); i++) {
*(src + i + len) = *(dest + i);
}
//加入字符串结束符号
*(src + i + len + 1) = 0;
}
int main() {
char a[100] = "hello";
char b[100] = "123456";
concatString(a, b);
printf("%s", a); //hello123456
return 0;
}
8.3 const修饰的指针变量
int main(void) {
//const修饰一个变量为只读
const int a = 10;
//a = 100; //err
//指针变量, 指针指向的内存, 2个不同概念
char buf[] = "aklgjdlsgjlkds";
//从左往右看,跳过类型,看修饰哪个字符
//如果是*, 说明指针指向的内存不能改变
//如果是指针变量,说明指针的指向不能改变,指针的值不能修改
const char *p = buf;
// 等价于上面 char const *p1 = buf;
//p[1] = '2'; //err
p = "agdlsjaglkdsajgl"; //ok
char *const p2 = buf;
p2[1] = '3';
//p2 = "salkjgldsjaglk"; //err
//p3为只读,指向不能变,指向的内存也不能变
const char *const p3 = buf;
return 0;
}
8.4 字符指针数组
- 表示为
char *ch[];
- 可以用来保存字符串的首地址
- 通过遍历每一个元素,在并在元素前加*,可得到每一个字符串;
// STATEMENT : 字符指针数组
#include <stdio.h>
//字符指针数组
//是一个数组,每一个元素是字符指针
int main() {
char *num[3] = {"heihei", "haha", "xixi"};
//定义一个指针保存num数组首元素的地址 &num[0] num
char **p = num;
for (int i = 0; i < 3; i++) {
// printf("%s\n",*(p+i));
printf("%s\n", p[i]);
}
printf("%c\n", *(*(p + 1) + 3));// *(p[1]+3) == p[1][3],a
//for (int i = 0; i < 3; i++)
//{
// printf("%s\n",num[i]);
//}
printf("%c\n", *num[0]);//h
printf("%c\n", *(num[1] + 1));//a
printf("%c\n", *(num[2] + 2));//x
return 0;
}
8.5 指针数组做为main函数的形参
int main(int argc,char *argv[])
- argc表示参数的个数
- *argv这个数组接收具体的参数
说明:
- main函数是操作系统调用的,第一个参数标明argc数组的成员数量,argv数组的每个成员都是char *类型
- argv是命令行参数的字符串数组
- argc代表命令行参数的数量,程序名字本身算一个参数