他们之间看似简单实则暗藏玄机!来看实践!
#include<iostream>
using namespace std;
int main() {
//指针知识小结: 指针提供了一种间接访问数据的方式
//1、指针的定义
int i = 100;
int* p = &i; //定义一个指向int类型对象的指针对象p,把i的地址放到p中,也就是说p指向了i
cout << *p << endl; //解引用操作符*,访问i的内容,输出为i的值
cout << p << endl; //输出内容为i的地址
///注意点:(1)若 * 或 & 紧跟在类型说明后,则定义的对象为指针或引用;int *p=&i;int &ref=*p;
//若出现在表达式中,则为解引用或取址符
///(2)定义指针对象时指针要和所指对象类型一致,void和基类指针除外;
//int i=0;
//double *p=i; 错误,类型不一致
///(3)定义多个指针时,对象名前面都要加上 * 号;
//int i, *p, *r;
///(4)定义指针时,若没有具体指向对象,则需要用 nullptr 来初始化;若一个未初始化的指针在一个语句块内部进行定义,则它存放的为一个随机值
int* ptr = nullptr; //空指针,没有指向任何对象
int* ptr1; //野指针,有潜在危险
//2、改变指向
int a = 10, j = 100;
int* p1 = &a, * p2 = &j;
p1 = p2; //改变p2的指向,也就是改变其地址
p1 = nullptr; //改变p1的指向,将其变成空指针
//注意:指针是可以加减来改变地址的,++p1;与p1=pi+1相同,增加一个地址单位(int为4byte),也叫指针移动,后面会详细讨论指针和数组之间的应用
//3、const与指针的使用
//(1)const可以修饰一个指针对象,使其成为一个指向const对象的指针,表明不能通过指针来修改对象的值(定义一个常量指针,* 号在const后)
const int ci = 10, cj = 1;
const int* p3 = &ci;
//*p3 = 0; 不能通过指针来改变对象的值
p3 = &cj; //但可改变指针的指向(也就是地址)
int r = 0;
p3 = &r;
// int* p3 = &ci; 一个普通指针不能指向const对象,常量地址只能赋给常量指针
//(2)定义一个指针常量,指向不能改变,但可通过指针来改变对象的值(* 号在const前)
int* const cptr = &i;
// cptr = &j; 不能改变指向
*cptr = 10; //但可改变对象的值
///(3)定义一个指向常量的指针常量(也就是指向和指针都不能改变,使用双const)
const int* const cp = &i;
//注:不要将指针常量和常量指针搞混了,记忆法: * 在前为指针常量(指向不能改),const在前为常量指针(值不能改)
// * 号在中间,const在两边,则为指向常量的指针常量(指向,值都不能改)
//4、类型推导
//(1)如果表达式的值是地址值,则auto可用来推导指针类型。auto可根据表达式的值推导出用户想要定义的数据类型,并用表达式的值初始化定义的对象
int u = 0;
const int ci1 = 100;
//auto p5 = &u;
auto p5 = &ci1; //光标放p处可查看其类型,auto将自动推导出指针类型
//注意:& 和 * 从属于对象名,而不是类型名的一部分,auto只是一个占位符
auto& re = u, * p6 = &u; //此处推导都为int型
//auto& r = i, p = &i; //此处显示有错误前后两个对象的类型不一致,不可用auto进行推导,可分开定义或像上面的方法加上*
//(2)用decltype进行指针类型推导(只想用表达式的类型而不想用其值来定义对象的关键字) 它会分析()内表达式或对象的类型来推导出后面对象的类型,但不会执行运算
int h = 0, * p7 = &h;
decltype (p7)pr1; //推导出pr1为int * 类型
decltype (*p7)ref = h; //ref为int & 类型,必须要初始化
decltype (*p7 + 1)s; //s为int类型
//5、void指针
// void指针是一类特殊指针,它可以指向任何类型的对象;并且它只是简单的将对象地址储存起来,但对对象的类型并不感兴趣
double x = 1.0;
int y = 20;
void* p4 = &y;
p4 = &y; //储存对象类型不限,可以存放其地址
//注:不能将void随意赋给一个普通指针,需要保证他们指向的类型要相同
double* ptrd = &x;
void* ptr2 = &x;
//ptrd = ptr; 此处显示类型不同无法转换,要借用强制类型转换(显性类型转换)使用static_cast 来执行操作
ptrd = static_cast<double*>(ptr2); //强制转换为double * 类型,当然两个指针所指的对象必须一致,不然会出现运行错误
//补充小结static_cast:
//(1)可执行浮点数操作
int i1 = 5, j1 = 3;
double k = i1 / static_cast<double>(j1); //强制将j1转化为double类型
//(2)有意将宽类型转化成窄类型
int f = static_cast<int>(i1 / j1); //将i/j的结果转化为int类型
//(3)const_cast常用来去掉对象的const属性,把一个const对象转化为非const对象,一般不提倡用这种方法。
//6、多级指针
//以二级指针为例,把一个指针对象的地址储存到另一个指针对象里,就成了指向指针的指针,这就为二级指针。
int i2 = 68, * ptrr = &i;
int** pptr = &ptrr;
cout << i << '\t' << *ptrr << '\t' << **pptr << '\n'; //以上方式均可访问i的值
//三级指针***,多级指针依次可推。
//数组小结:
//数组是有限个同类型元素的有序集合,并且所以元素都顺序存放在一段连续的空间内。
//1、数组的定义和初始化
int arr[5] = { 1,2,3,4,5 }; //定义一个含五个int类型的元素的数组 (直接定义) int arr[5]={0};或int arr[5];
const int sa = 10; //或者 constexpr int sa=10;
int ard[sa];
short things[] = { 1,3,5,7 };
//注意:(1)sa必须是一个常量,不可以为变量,所以要用const或constexper;并且数组长度(下标)必须为整型。
//(2)数组下标为常量,元素下标可为变量,ard[i],ard[2]。
//(3)数组元素从[0]开始,到[sa-1]结束,不包括ard[sa]。
int arr1[] = { 1,2,3 };
//(4)定义时可不用写下标,系统会自动推导元素个数,未给出的元素默认为0。
//(5)只有在定义数组时才能使用初始化,此后就不能使用了,也不能将一个数组赋给另一个数组。
//(6)数组名就可代表数组的首地址(一种映射)。
//补充C++11的数组初始化方法
//(1)初始化时,可以省略等号
short thing[10]{};
int counts[]{ 0,1,2,3,4 };
//(2)列表初始化禁止缩窄转换
long plifs[] = { 25,92,3.2 }; //这条语句是不能通过编译的,将浮点数转换为整型是缩窄操作
char slifs[4]{ 'h','i',1122011,'\0' }; //这句也是不能通过编译的,1122011超出了char变量的取值范围,只能打印出前两个字符
char tlifs[4]{ 'h','i',112,'\0' }; //可以通过编译,112在char的范围内
cout << '\t' << tlifs << endl; //输出为 h i p(112)
//2、复杂数组的定义
int* arrp[5]; //定义一个含5个int类型元素的数组,每个元素都是指针(都是int * 类型)
int(*parr)[5] = &arr; //定义一个指向含5个int类型元素的数组的指针;parr的初始值为arr的地址(为第一个元素的首地址)
int(&rarr)[5] = arr; //定义arr的一个引用
int* (*parrp)[5] = &arrp; //parrp为指向指针数组arrp的指针;arrp的类型就为int * 所以前面要用int *
int* (&rarrp)[5] = arrp; //rarrp为指针数组arrp的引用
//3、访问数组元素(一维)
//(1)可以通过下标操作符[]来访问数组元素
arr[0] = 10; //对第一个元素进行写操作
cout << arr[0] << '\t' << arr[2] << endl; //读操作,输出第一个元素和第三个元素
//(2)由于数组元素数量是确定的,所以也经常会使用for语句来访问数组元素,传统的for循环语句可以实现,
//C++11中进入了范围for(range for)语句,其用法更为简洁和快捷,可用来遍历数组或其他数列的所以元素
// for (decl : expr) { exper必须为一个对象序列,数组,容器(vector)或字符串(string),
// statement; deal是与对象序列中元素相同的对象,一般用auto来自动推导数据元素类型
//} statement为执行语句
for (auto i : arr) {
cout << i << endl; //i为arr中当前元素的副本,注意这里只是对arr进行了读操作,i不能对其元素进行写操作
}
//若要对其进行写操作,就要将i声明为引用
for (auto& i : arr) {
i = 'a'; //写操作,将每个元素都变为97
cout << '\t' << i << '\n';
}
//4、多维数组(指数组中的元素类型都是数组类型)
int a2d[3][5]; //定义一个二维数组,可看做三行五列,但他们还是在一个空间内连续储存
int a3d[2][3][5]; //定义一个三维数组
//注意:多维数组中最左边的对应最高维(第一维,第二维....),二维数组可对应矩阵(第一维为行,第二维为列)
//(1)多维数组的初始化(依然可以使用列表方式来初始化)
int a4d[3][5] = {
{0,1,2,3,4},
{3,4,5,6,7},
{2,1,7,9,3} }; //也可省去内部的{},直接打出元素(按顺序) 直接初始化
int a5d[3][5] = { 0,1,2 }; //显式初始化部分数组元素,只初始化了第一维中的前三个元素,则其他元素默认为0
int arrd[][5] = { {0},{1},{2} }; //显式初始化每一维的第一个元素,也就是每一行中的第一个元素
//注:第一维的下标可以不写,但要明确(在后面的{}中)说明维数和每维中元素的个数(未给默认0),才能自动推导出下标;且仅第一维的下标可以省略
//例:a5d中第一个下标不能省略,若省略则前面自动推出是[1],而不是[3]
//(2)访问多维数组元素
//<1>依然可以像一维一样用下标来访问元素
cout << a4d[2][1] << endl; //访问第二行中的第二个元素
//<2>利用范围for语句来处理多维数组,注意:除最内层的循环外,其他各层循环中必须使用引用
for (auto& r : a4d) { //r被推导为int (&)[5]类型
for (auto c : r) { //r为一维数组的引用,r为列表类型才能用于范围for语句中,所以外层都要用引用
cout << '\t' << c << endl;
}
}
//注意:for语句的用法要正确,后面对象要为列表类型;中间为 : 号,不是 ; 号。
//5、指针与数组
//指针指向数组一般都是指向第一个元素的地址;并且数组名通常会转化为第一个元素的地址,是个右值
//(1)指针指向一维数组
int ars[]{ 1,2,3,4 };
int* ppr = ars; //此处指针指向数组时,不需要用&,数组名会转化为第一个元素的地址
//上述式子也等价于 int *ppr=&arr[0];
cout << ars << '\t' << &ars[0]; //这两个输出结果都是第一个元素的地址,也可得出一个小结论:& 与 [] 有相互抵消的作用
// 例:二维数组中,arr2[0]与&arr2[0][0]是相同的(也就是说第一个元素的地址就代表第一行的地址)
auto pa = ars; //可用auto来自动推导出pa是一个int *类型的指针,指向arr[0]
auto pi = &ars; //若在数组前加上 & ,则推导出来pi是一个int (*pi)[4]类型,为一个指向含有4个整型元素的数组的指针
auto po = *ars; //此时po的类型就为int,* 用在表达式中为解引用,*ars就为int ars[4]类型,准确来说,ars代表着第一个元素的地址,所以*ars=ars[0]
cout << '\n' << po << endl; //输出内容为ars[0]=1
//(2)指针指向二维数组
int atd[3][4] = { {3},{0} };
int(*prt)[4] = atd; //指向atd的第一个元素,注意:此处和一维数组定义是不同,一维定义需在atd前加上 & 号,但二维却不用;因为一维不加的话类型不匹配
//上述语句也等价于 int (*prt)[4]=&atd[0]; 第一个元素的地址
//注:上面语句中的()不可缺少,若缺少,则会变为另一种定义
int* prt1[4] ; //这时prt1就为一个含有4个指向整型对象的指针类型的元素的数组
//注:一个数组名可以理解为一个const指针,但两者并不完全等价
int* const pl = atd[0]; //二维数组中,其实就是指向atd中的第一个元素
int* const pk = &ars[0]; //一维数组中 所以arr也可以理解为const指针pk,此指针输出为第一个元素
cout << *pl << '\t' << *pk << endl; //输出各数组中的第一个元素
//(3)指针访问数组
//<1>访问一维数组 当一个指针和一个数组关联起来时,就可以通过指针来访问数组了
int arr2[] = { 1,2,3,6,5 };
int* pn = arr2;
//注:若想用一个指针指向一个数组,则不需要用 & 号,直接写数组名就可;若指向单个元素或单个数时,要加上&
///此处引入指针运算
//[1]指针的移动
int* pn1 = pn + 2; //将pn指针的地址向后推两个单位,再赋值给pn1指针,此时pn1指针指向arr2[2],即第三个元素
int* pn2 = pn++; //注意++的位置,此时先把pn的地址赋给pn2,再将其向后推一个单位,此时pn指向arr2[1]
int* pn3 = ++pn; //此时pn继续在向后移一个位置,pn3和pn均指向arr2[2](上面的语句执行后pn已经向后移了一个位置,注意不要忽略)
//[2]指针的关系运算 (支持所有关系运算符)
//例:p==p2,p>p2,p3<=p;
//[3]指针相减
//两个指针相减的结果为所指数组元素的位置距离,例:pn3-pn2的结果为2
///注意点:(1)指针在运算时,下标不能越界
pn =&arr2[0];
pn = &arr2[5]; //指向尾元素后面的一个位置,也就是pn=arr2+5; 虽然不存在arr2[5]这个元素,但仍可计算出该位置的地址
//(2)仅当两个指针指向同一个数组时,它们之间的运算才有意义
// 访问实践
int val = *(pn + 2) + 1; //此语句等价于:int val=arr2[2]+1;
int val2 = pn[1]; //可将下标应用于指针 int val2=arr2[2];
cout << '\n' << val << '\t' << val2<<endl;
//<2>访问二维数组
//[1]可以利用指针访问一维数组类似的方法,来访问二维数组
int aed[3][5] = {0};
int(*ped)[5] = aed; //定义一个指向二维数组aed的第一个元素
ped[1][1] = 1;
*(*(ped + 1) + 1) = 1; //上述两个语句表达的意思相同,把第二行第二个元素赋值为1,下面的语句从里往外看
//(ped+1)代表移动一行,此时指针地址变为第二行第一个元素,*(ped+1)+1 代表第二行第二个元素的地址,*(*(ped+1)+1)代表第二行第二个元素
cout << ped << '\t' << *ped << '\t' << (ped + 1) << '\t' << *(ped + 1) << '\t' << (*(ped + 1) + 1) << '\t' << *(*(ped + 1) + 1) << endl;
//注意:输出内容,ped和*ped都为第一行第一个元素地址;(ped+1)和*(ped+1)均为第二行第一个元素的地址;(*(ped+1)+1)为第二行第二个元素的地址,*(*(ped + 1) + 1)为第二行第二个元素的值
//上述有的 加* ,有的不加* ,却输出一样,这和定义的指针数组有关,要注意和其他情况区分
//[2]可用auto和for语句来访问数组
int a22d[3][5]{ {1},{2},{3} };
for (auto p = a22d; p < a22d + 3; ++p) {
for (auto q = *p; q < *p + 5; ++q) {
cout << *q;
}
cout << endl;
}
//同时还可利用数组元素的连续性来访问
for (auto p = &a22d[0][0]; p < a22d[0] + 15; ++p) {
if ((p - a22d[0]) % 5 == 0) {
cout << endl;
}
cout << *p;
}
return 0;
}