目录
指针概述
指针即指向一块内存空间地址的变量,指针是变量
C语言中所有类型包括自定义类型的变量甚至指针变量本身都拥有对应的指针,只要在变量声明时在变量名左边加上*即声明了一个指针
指针变量
指针变量的声明
指针变量的声明与普通变量的声明大差不差,只是在变量名左边多了一个*而已
int a;
int* pa;
char c;
char* pc;
struct S s;
struct S* ps;
声明一个指针变量就是这样的简单
将变量的地址存放入指针变量中
将一个地址放入一个存放地址的变量中很轻松,我们只需要使用取地址操作符 '&' 这个操作符拥有一个操作数,为右值,其返回值为右值的地址,用赋值操作符我们就可以将一个变量的地址放入一个指针变量中了;
请在正常情况下将对应变量的地址放入对应类型的指针变量中,除非你是故意这样做的而不是不小心,但这样你的IDE一般会提醒你类型不兼容,你可以选择使用强制类型转换来解决这些问题
int a;
int* pa = &a;
char* pca = (char*)&a;
指针的解引用
通过指针,我们可以访问到指针所指向的那一块内存空间,访问的步骤就是解引用,解引用过后就能够直接访问指针指向的变量了,访问方式为指针对应的类型而不一定是这块内存空间原本的访问方式
//平台为x86 小端存储
int a = 0;
int* pa = &a;
char* pca = (char*)pa;
*pa = 0x12345678;
printf("%x\n", a); //0x12345678
*(pca + 2) = 0;
printf("%x\n", a); //0x12005678
指针与sizeof
指针变量的长度会根据平台的位数而改变
- 16位平台指针的长度为 16bit/2byte
- 32位平台指针的长度为 32bit/4byte
- 64位平台指针的长度位 64bit/8byte
sizeof对于指针类型与指针变量的计算时,计算出来的大小只与平台的位数有关,与指针变量的类型无关,也就是说无论是什么样的指针变量,它们的大小始终都是4/8字节,大小随平台而变化
不同的指针变量大小代表着内存的最大物理上限的不同,32位平台也就是x86平台的物理内存容量上限为4GB,64位平台就是...4GB*4GB这么大
int* pi;
printf("%d\n", sizeof(pi));
printf("%d\n", sizeof(int*));
printf("%d\n", sizeof(char*));
printf("%d\n", sizeof(struct S*));
//它们的大小都为统一的4/8字节
指针的加减运算
指针加减整数
指针加减整数得到的值为在当前指针处向前后移动一个指针类型指向的内容的类型的大小,例如int*类型的指针变量增加整数1得到的是该指针向后移动一个int的长度的值:
如果一个指针的类型是 int* 假设它存储的地址为0x00000000,那么它增加1得到的地址是0x00000004;如果这个指针的类型为char*,则增加1得到的地址就为0x00000001
int a = 0;
int* pa = &a;
char* pca = &a;
printf("%p %p\n", pa, pa + 1); //pa + 1 挪动了4字节
printf("%p %p\n", pca, pca + 1); //pca + 1 挪动了1字节
指针减去整数同理,如需校验请自行校验,我在此就不再重复篇幅了
同类型的指针减去指针
指针减指针只在同类型指针之间才能够得到正确结果,而这个结果为两指针之间相差元素的数量
char sz[512] = "Hello World\n";
printf("%d\n", &sz[10] - &sz[0]); //结果为9,这两个地址之间拥有9个char类型的元素
指针无法加上指针,编译器一般会直接报错
指针与数组
数组符号与指针的关系
所谓的数组符号,即是在声明与使用数组时都会用到的方括号 "[]"
虽然数组与指针本质不同,数组拥有切实的空间,但我们可以将其与指针看作类似原理来学习
- 声明一个数组
- 声明一个数
int arr1[10]; int arr2[] = { 1, 2, 3 }; int arr3[5] = arr2;
- arr1 声明一个整形数组,这个数组的空间为10个整形的大小, 不进行数组的初始化
- arr2 声明一个整形数组,但是并未在 "[]" 中指定数组的大小,而是通过初始化列表中的元素个数来计算得出数组的大小为 3 个元素
- arr3 声明一个整形数组,给出确定的元素个数为5, 初始化列表为数组 arr2,而数组 arr2 中的元素数量为3,所以在填充 arr2 中的固有元素后将 arr3 中剩元素都初始化为0,如果想要数组直接全部初始化为0,可以直接在初始化列表参数填一个0;
- 初始化列表
- 初始化列表的书写语法
{ 1, 2, 3 }
- 初始化列表可以用作变量的初始化,需注意:即便初始化列表能够用作很多变量的初始化,但其会因为变量本身的长度限制而限制列表的长度;例如:
- 单个变量使用初始化列表时,列表内只支持一个变量或常量
- 数组使用初始化列表进行初始化时,列表的内容数量不能够超过数组的成员数量
- 结构体使用初始化列表进行初始化时,可以使用 "." 操作符对初始化参数的成员归属进行指定,无法对不存在的成员进行初始化
- 初始化列表的嵌套与有序性
- 初始化列表支持嵌套, 嵌套初始化列表多用在结构体数组中
- 初始化列表无论是否嵌套,都具有有序性
- 下列代码可以填充一个二维数组,只要数组能够容纳下这个初始化列表中的内容,那么无论这个数组的大小是怎样,都将一组一组地顺序填充
{ { 1, 2, 3}, { 4, 5, 6 } } { { 1, 2 }, { 3, 4 }, { 5, 6 } }
- 初始化列表的书写语法
- 声明一个数
- 使用数组符号读写数组元素内容
- 使用数组符号读写数组元素的方法
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; arr[3] = 0; //读写数组arr中下标为3的元素
-
不使用数组符号读写元素的方法
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; *(arr + 3) = 0; //读写数组arr中下标为3的元素
-
这两种读写方法的区别
-
可以看到 arr[3] 几乎等同于 *(arr + 3),但是这两种方法还是有一点区别的
-
arr[value] 调用的这种方法在value超过数组的元素个数的时候编译器会直接报错
-
*(arr + value) 调用的这种方法在value超过数组的元素个数的时候编译器并不能够准确检测到其中的逻辑错误,从而导致野指针的问题
-
- 使用数组符号读写数组元素的方法
指针与字符串
一个常量字符串,也就是 "Hello World\n" 这样的一个字符串,它的类型可以理解为为 const char*
也就是它的类型可以理解为一个常量字符指针
我们可以通过一个 const char* 变量拿到它的地址从而在Debug模式下的编译器中观察这个字符串的结构
可以看到,所有字符串的结尾都自带一个 '\0' 且我们并未主动添加这个字符 所以所有字符串都是以 '\0' 结尾的;理解了这一点之后我们就能够知道字符串函数的运行原理了,它们都在 string.h ,这些函数大都需要判定 '\0'
指针与数组的组合
如果我们需要存放很多的指针,我们可以声明一个存放指针的数组,称作指针数组;指针数组书写形式为:
int* arr[10];
这是一个数组,它存放了10个元素,每个元素的类型为 整形指针 int*
如果我们需要存放一个数组的地址,我们可以声明一个存放数组地址的指针变量,称作数组指针;数组指针书写形式为:
int (*arr)[10];
这是一个指针,它存放的是一个指向拥有十个元素且每个元素类型为 int 的数组
如果我们需要寻访很多的数组,我们可以声明一个存放数组指针的数组,称作数组指针数组;其书写形式为:
int arr1[10]; int arr2[10]; int(*arrArr[2])[10] = { arr1, arr2 };
arrArr是一个数组,这个数组拥有两个元素,这两个元素的类型都是一个数组指针,这个数组指针指向的是拥有10个元素且每个元素都为整形的数组
如此往复,我们可以用指针存放无比复杂的结构的地址
函数与指针与类型重命名操作符
函数指针类型简介
函数指针类型就是指向函数的指针类型,函数名的类型就是函数指针类型,函数名就是这个函数的地址,函数指针的使用不需要解引用操作,当然你也可以解引用后再使用。
函数指针变量与函数的声明
- 函数指针变量的书写形式为:
返回值类型 (*变量名)(参数列表) int (*fFunc)(int, int)
- fFunc变量能够存放一个返回值为 int 参数列表为两个 int 类型变量的函数的地址
- 函数声明的书写形式为:
返回值类型 函数名(参数列表); int FuncName(int val1, int val2);
- 声明函数后可直接通过函数名调用该函数
函数指针的嵌套
函数指针可以嵌套函数指针类型,也就是返回值类型或参数列表内的参数类型可以是函数指针类型
int* (*(*fFun)(int*, int*))(int, int)
//fFun是一个函数指针类型,它的参数列表是两个int* 类型的指针变量
//它的返回值类型为 int* (*)(int, int) 这样一个类型的函数指针
以上,参数列表为函数指针的情况同理,可自行验证与研究
函数指针与回调函数
将函数指针作为变量列表中参数传递给某个函数供其调用的情况被称作回调,这个被调用的函数被称为回调函数,例如 qsort
再例如main函数,main函数就是一个回调函数,只不过是由系统直接调用的特殊回调函数,main函数的类型可以为 int (*)() 或者 void (*)(),其它类型的由于我写得不多所以就不列举了
typedef关键字重定义类
- 有一些类型名太过冗长,我们可以使用该关键字对其进行重定义以缩减其长度
- 如果我们需要对某些特殊形式的变量进行特殊对待,我们可以使用类型重定义,这样就算用原本的类型也会报类型兼容的错误,相当于强制使用该类型变量(或许我们可以强制类型转换),这种特殊类型也很常见,比如 size_t,他在 stdio.h 中定义 我们使用printf打印它的时候组要使用 %zd 来表示size_t类型的变量
- 类型重定义的书写形式:
typedef 类型 新的类型名; typedef unsigned int mySize_t;
- 类型重定义支持逗号表达式:
这些类型名都代表 signed chartypedef 类型 新的类型名1, 新的类型名2, ...; typedef signed char CHAR, Char, cHar, chAr, ...;
类型名重定义与指针的结合
我们可以重定义函数指针类型或其它指针类型,从而让这些指针更加直观
- 普通指针的重定义
typedef signed char CHAR, *lpCHAR; typedef signed int *lpINT;
- 函数指针的重定义
typedef int (*重定义后的类型名)(int, int); typedef int (*TFun1)(int, int); typedef TFun1 (*TFun2)(TFun1, TFun1);
TFun2这个类型是一个函数指针类型,其返回值与参数列表都为 TFun1 这种函数指针类型;如果不使用类型重定义,则会变成这个样子
int((*fFunName)(int(*)(int, int), int(*)(int, int)))(int, int)
不能说非常难看吧,只能说非常不好读
- 重定义后类型的使用
- 类型重定义之后的使用就和普通变量的使用相同,声明与使用都与普通变量没有太大的区别,只是一个简单的类型名可以代表一个非常复杂的类型,仅此而已
//假设上面的重定义沿用到该代码中 int i = 10; lpINT lpI = &i; TFun1 tempFunc1 = ... ; *lpI = lptempFunc1(1, 2); TFun2 tempFunc2 = ... ; tempFunc1 = tempFunc2(tempFunc1, tempFunc1);
- 类型重定义之后的使用就和普通变量的使用相同,声明与使用都与普通变量没有太大的区别,只是一个简单的类型名可以代表一个非常复杂的类型,仅此而已
多级指针
多级指针之间的关系
- 解引用的本质就是访问指针变量所存储的内存地址并将这个内存地址内指定字节的内容返回
- 每以级指针可以存放上一级指针的地址
- 每一个指针解引用可以得到上一级指针
- 如果没有上一级,则为一级指针变量修饰的内容
多级指针的分辨与声明与使用
简要来说,每一级指针在声明的时候都拥有一个 * 操作符,如果是多级指针,就拥有多个*
如果想要得到多级指针最深层藏着的信息,则需要对应声明时*的个数次数的解引用
int a = 0;
int* pa = &a;
int** ppa = &pa;
printf("a = %d\n", a);
printf("**ppa = %d\n", **ppa);
**ppa = 20;
printf("通过多级指针更改后\n");
printf("a = %d\n", a);
printf("**ppa = %d\n", **ppa);
特殊指针
void*指针
void* 类型指针是一类兼容性极强的指针类型,它可以接受任意指针类型的赋值,但它也有缺陷:由于它能包容特别多的指针类型,导致解引用无法正确使用而不能够直接对void*类型指针进行解引用操作
char* pCh = "Hello World"; int* pInt = NULL; double* pDb = NULL; void* temp = NULL; temp = pCh; temp = pInt; temp = pDb; //如果想要知道这个指针所指向的内容是什么,就需要有明确的访问方式,例如: printf("%c\n", *(char*)temp); printf("%d\n", *(int*)temp); printf("%llf\n", *(double*)temp);
void* 类型通常出现在内存操作函数中
- 当它存在于参数列表中时,它们通常不关心类型与内容数据,我们需要的是填入一个任意类型的指针和一些限制数据,常见于内存运算系列函数
- 当它是某个返回值时,我们想要获取返回的内容通常也需要强制类型转换为我们所需的类型,常见于内存申请系列函数
char*指针
char* 类型指针也很神奇,因为指针解引用会根据指针指的基本类型而决定访问内存空间的大小于访问方式
- unsigned int* 的指针解引用之后这块内存空间的读写形式为unsighed int,它读取4字节空间
- char* 类型的指针一般我们并不会仅仅将其用于对char类型变量的地址进行使用,而是将其当作对一个 1字节基本宽度的空间的使用,使用时我们一般也不会太过关注其内容,顶多看看这个字节的空间是否为空,内存运算系列函数会使用一个void*类型的参数与一个名字意思为 单个元素长度 的参数对内存空间内的数据进行运算,单个元素长度为n的话,那么每进行依次运算,这个void*类型的参数就需要进行大概时如下运算的运算:((char*)ptr) += n
NULL指针
NULL 指针一般称作空指针,在不同的语言都不约而同的拥有类似的东西,例如C++中的 nullptr 与Java中的 null,其形式不同,但有具有相似的意义——表明一个不可以被使用的变量或对象或其它,总之都是代表着没有经过手动初始化或未给予明确值的意思
在C语言中,NULL 的原型为 ((void*)0) 这意味着NULL是一个指针,在很多函数中NULL都拥有特殊意义:
- 有的出现在参数列表中,表示默认或不要重置等操作;
- 有的出现在返回值中,表示这个本该返回一个指针的函数没有正常执行
指针与const
const限定符与它的作用范围
const限定符的作用为右值加了一把无法更改的锁,由于const限定符在同一条语句中只能出现一次,我们就得明确的区分该限定符到底限定了谁
const限定符修饰指针类型
当const限定符在变量类型的左侧或中间时,const修饰符就限制了指针变量,使其指向的内容无法被更改
const char* ch1 = NULL;
char const* ch1 = NULL; //这两个变量声明效果相同
ch1 = (char*)...; //ch1的内容可更改
*ch1 = '\n'; //错误,ch1指向的内容无法被更改
此时我们就不能够去更改ch1指向的内容了,但ch1的内容依旧可以改变
const限定符修饰指针变量
当const限定符在变量名左边第一个位置时,该限定符就将限定该变量,使这个变量的内容无法被更改,但由于这个变量是一个指针变量,所以它所指向的内容是可以更改的;
char* const ch2 = &(...);
ch2 = NULL; //错误的,ch2的值无法被更改
*ch2 = '\n'; //ch2指向的内容可以被更改
引用
(stdio.h) - C++ Reference (cplusplus.com)https://legacy.cplusplus.com/reference/cstdio/ (stdlib.h) - C++ Reference (cplusplus.com)https://legacy.cplusplus.com/reference/cstdlib/ (string.h) - C++ Reference (cplusplus.com)https://legacy.cplusplus.com/reference/cstring/
碎碎念
有目的地实践加上无目的地摸索就是最好的学习方式,看完之后将疑惑一同乱炖就能得到想要的结果,加油
...
...
...
梦晞最近在和感冒近身搏击,做梦手上都有三根吊针,隐匿了好久真的很抱歉555
感冒一个月了已经,稍有好转,身体快撑不住了
画师:WaterSnake 水蛇