什么是指针?
阿宋认为指针就是一种复合类型的变量。它由两部分组成,一部分是指针的本体,一部分是指针指向的对象。我们可以通过指针去改变指针指向对象的值,也可以更改指针所指向的对象。
拿一般的单一类型的变量类型举例,char类型变量的内存中存放的是char类型的数据。int类型变量的内存中存放的是int类型的数据。而对于指针来讲,指针类型的变量内存中存放的是一个地址,而这个地址指向的才是存有对应数据类型的数据。
要养成的一个习惯便是,看到一个指针,第一反应就是这个指针的值是一个地址。
&操作符与*操作符
要想更好的去掌握指针,我们就必须先搞清楚&操作符与*操作符的含义。
&操作符:取址操作符,给出&操作符后的地址/获取对象的指针
int main()
{
int a =5;
double b = 10.4;
cout<<"int类型的变量a它的地址是:"<<&a<<endl;
cout<<"double类型的变量b它的地址是:"<<&b<<endl;
}
输出内容:
int类型的变量a它的地址是:0x7ff7b82503ac
double类型的变量b它的地址是:0x7ff7b82503a0
*操作符:间接访问操作符/解引用操作符,简单点来说就是得到地址指向的那个值
int main()
{
int a =5;
double b = 10.4;
cout<<"int类型的变量a它的地址是:"<<&a<<endl;
cout<<"int类型的变量b它的地址是:"<<&b<<endl;
cout<<"a变量地址中存放的值是:"<<*(&a)<<endl;
cout<<"b变量地址中存放的值是:"<<*(&b)<<endl;
}
输出内容:
int类型的变量a它的地址是:0x7ff7b82503ac
double类型的变量b它的地址是:0x7ff7b82503a0
a变量地址中存放的值是:5
b变量地址中存放的值是:10.4
通过我们上面的这个编程案例讲解,阿宋相信聪明的同学都已经了解了这两个运算符的含义和用法。接下来就可以去讲指针的其他用法了。
指针的创建
指针声明的模版:
变量类型* 指针名;
其中 * 星号是必须加的,目的是告诉对方你声明的变量是一个指针。星号后面空格后紧跟的就是指针的名字。
前面说过,指针中存放的是一个地址,必须要用地址去初始化这个指针。那么这里&取地址符就发挥了作用。
定义指针的模版:
变量类型* 指针名 = &对应的变量类型(与前面的变量类型相同)
下面就随便演示创建几个指针:
int main()
{
int a = 5;
double b =10;
int* aa = &a;
double* bb = &b;
cout<<"指针的值是一个地址,所以aa = "<<aa<<endl;
cout<<"指针的值是一个地址,所以bb = "<<bb<<endl;
}
//输出内容:
指针的值是一个地址,所以aa = 0x7ff7b36603ac
指针的值是一个地址,所以bb = 0x7ff7b36603a0
指针值
一个指针的值(即指针存放的地址)应该属于下列的四种状态之一:
- 指向一个对象(指针的值就是对象的地址)
- 指向紧临对象空间的下一个位置(有点next指针的意思)
- 空指针,意味着指针没有指向任意的对象
- 无效指针,也就是不属于上述情况的任意一种情况
空指针和void*指针
如果说我们想要创建一个指针而不让它指向任何对象。我们可以让他等于0或NULL。C++11新标准中规定,我们可以使一个指针等于nullptr,而去代表它是一个空指针。
void*指针是一种比较特殊的指针类型。它与普通指针不同的是,它可以存放任意对象的地址。
创建指向指针的指针
和创建一个普通的指针不同,创建指向指针的指针有两个*星号,具体事例请看下面的例子:
int main()
{
int a = 5;
int *aa = &a;
int **aaa = &aa;
cout<<"输出指针 a指向对象的值: "<<*aa<<endl;
cout<<"输出指针 aa的地址"<<&aa<<endl;
cout<<"输出指针 aaa的值"<<aaa<<endl;
cout<<"输出指针 aaa的地址"<<&aaa<<endl;
}
//输出结果
输出指针 aa指向对象的值: 5
输出指针 aa的地址0x7ff7b1ab33a0
输出指针 aaa的值0x7ff7b1ab33a0
输出指针 aaa的地址0x7ff7b1ab3398
从上面这个例子我们可以更清晰的看懂指针的概念,也能更好的去理解&取地址符和*解引用符的作用。
因为指针也是变量的一种,所以指针自然也有自身的内存地址。我们需要注意的就是不要把指针自身的地址和指针的值的地址这个概念混淆了就好。
指针和数组
易错点:指向数组的指针和含有指针的数组
我们很容易将指向数组的指针和含有指针的数组这两种东西所相互混淆,那么为了将它们区分开来,阿宋来给大家讲一讲我是怎么区分的
案例一:
int *a[10];
这个意思是一个含有10个指针的数组,数组的名字是a
案例二:
int (*b)[10] = &arg
这是一个指针,指向一个有10个int元素的数组arg。
案例三:
int (&c)[10] = arg;
这是一个引用c,它与一个有10个int元素的数组绑定在一起。数组名字是arg。
案例四:
int *(&d)[10] = ptr;
这是一个引用d,它与含有10个指针元素的数组ptr绑定在一起。
那么这里阿宋的理解就是如果没有看到括号,那这就是一个含有指针的数组。
如果看到了括号就从里往外去读。比如案例2,3,4。括号里是一个指针,那么这就是一个指向数组的指针,指针名就是括号里的名字。如果是一个引用,那这就是一个绑定数组的引用,引用名就是括号里的名字。
指针在数组上的应用
在C++的语言中,指针与数组的关系特别的紧密,当我们用一个指针指向一个数组时,默认指向的是数组的首元素。比如:
int main()
{
int a[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int *b = a; //指针b指向了a这个数组或者是指针b指向了数组a的首元素
int *c = &a[0]; //这两个的意思是一样的。
cout << *b << endl;
cout << *c << endl;
}
//输出结果:
0
0
指针也是迭代器
指针和迭代器的作用相同,我们可以通过指针去遍历一个数组的元素。这里就不细细展开了,上一个例子也就都懂了
int main()
{
int a[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int *b = a; //指针b指向了a这个数组或者是指针b指向了数组a的首元素
b = b + 5;
cout << a[0] << endl;
cout << *b << endl;
}
//输出结果
0
5
概念剖析:指向指针的数组
我们之前讲过,指针的值是一个地址,那么这个地址是对象的值的地址。我们要用取地址符去获得这个对象的地址然后将它去初始化这个指针。
那我们再来看指向数组的指针。那么首先数组是一种数据结构,数组的值的地址与数组本身的地址是一个地址。
int main()
{
int d[]={5,6,7};
int *c = d;
//也可以这么去写 int (*c)[3] = &d;
cout<<d<<endl;
cout<<c<<endl;
cout<<*d<<endl;
cout<<*c<<endl;
}
//输出结果
0x7ff7b730239c
0x7ff7b730239c
5
5
来我们看这个输出结果,因为数组的值,就是数组首个元素的值的地址,而指向数组的指针呢?其本质也是指向了数组中第一个元素的值,所以它的值就是数组元素中第一个元素的值的地址。所以我们分别取输出 c d 得到的都是数组d中第一个元素的地址。
继续看,我们用间接访问符 * 去访问c d中存放的地址所对应的值,得到的答案也就是数组中第一个元素5。得证。
概念剖析:指向多维数组的指针
我们刚刚剖析了指向数组的指针这个概念后,我们再来去学习指向多维数组的指针的这个概念就容易多了。
这里我们再次强调一些基础的概念,希望大家不要混淆。
- 指针是一个变量,指针的值是一个地址,指针本身也有一个地址,它们不一样!!!我们可以通过&取地址符去获取指针的地址。
int main()
{
int a =5;
int *b = &a;
cout<<b<<endl;//输出的是a的地址
cout<<&b<<endl;//输出的是指针b的地址
}
输出结果:
0x7ff7bbf073ac
0x7ff7bbf073a0
- 而数组其实是一种数据结构,数组本身的地址就是它第一个元素值的地址,所以取a的地址和输出a的值的结果是一样的
int main()
{
// int a[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
// int(*b)[3][3] = &a;
// int(*c)[3] = *b;
// cout << (*b) << endl;
int a[] = {4,5,6,7};
cout<<a<<endl; //输出数组a的值
cout<<&a[0]<<endl;//输出数组a首元素的地址
cout<<&a<<endl;//输出数组a本身的地址
cout<<*a<<endl;//输出的就是a的首元素的值
//cout<<*a[0]<<endl;//错误,a[0]本身就是个整数,不可以对整数再使用间接访问符
}
看完了上面繁琐的证明后,我们再来去看看指向多维数组的指针。
假设有一个指针b,指向了一个二维数组a,那么指针指向的其实就是a的首元素,那么这个指针的值就是a的首元素,a的首元素是什么呢?a的首元素是一个数组。数组本身就是一个地址,是这个数组首元素的值的地址。那么我们是不是就可以得出一个结论。指针b,指向的就是二维数组a的首元素的首元素。我们来编程看一下我们得到的结论对不对:
int main()
{
int a[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};//a是一个二维数组
int(*b)[3][3] = &a;//b是一个指向二维数组的指针。它指向的其实是二维数组a的首元素,
//也就是一个含有3个元素的数组
int(*c)[3] = a;//另一种写法
cout<<*c<<endl;
cout << *b << endl;//输出b的值地址
cout<<&a[0][0]<<endl;//输出二维数组a首元素的首元素的值的地址
}
结论得证;
指针和const
这里我认为是理解const对象的一个重难点。
因为指针本身是一个对象,它又可以指向另一个对象。因此,指针本身是不是常量,和指针指向的对象是不是常量就变成了两个问题。
我们为了使这个概念更加的清晰,我们用 顶层const 、与 __底层const__这两个名词来区分这两个问题。
顶层const:代表的是指针本身就是个常量。
底层const:代表的是指针指向的对象是一个常量。
为了更好的理解这个概念,我们接下来举上几个例子来区分顶层const与底层const
例1:
int i = 5;
int* const p1 = &i;
*p1 = 7;正确,因为变量i不是一个常量,所以可以使用指针去修改i这个变量的值
在这个例子中,p1是一个顶层const,它代表的是p1这个指针本身是个常量,所以说,p1不可以更改它所指向的对象,只能指向i。
在这里面,i是常量和不是常量都可以,因为指针本身是个常量与指针指向的对象是不是常量无关。表示顶层const的方法是将const关键字写在变量类型的后面。
例2:
const int ci = 7;
const int* p2 = &ci;
const int c2 = 8;
p2 = &c2;//正确
*p2 = 9;//错误,因为指针指向的值是一个常量,所以不能用指针去修改一个常量的值。
在这个例子中,p1是一个底层const,它代表的是p1这个指针指向的对象是一个常量,而p1本身并不是一个常量,所以p1可以指向任意的int类型的常量对象。表示底层const的方法是将const关键字写在变量类型的前面。
注意⚠️:我们把指向常量的指针叫做常量指针,但是常量指针也可能指向一个非常量的值,但指针以为自己指向的是一个常量,所以指针不能去修改这个常量的值。这一点与常量引用类似。所以下面这个小例子也是对的。
这就是非常重要的一条关于初始化的规则:我们可以用一个非常量的值去初始化底层const,但是反过来则不行。
int ci1 = 9;
const int* p4 = &ci1; //正确,底层const,指针以为自己指向的是一个常量,但其实不是。
ci1 = 10;//我们不可以通过指针去修改变量的值,但我们可以通过更改变量本身的值去修改变量的值,hh
例3:
const int ci = 7;
const int* const bi = &ci;
int i = 5;
bi = &i;//错误,指针本身是一个常量,不可以更改指针所指向的对象。
*bi = 6;//错误,指针指向的对象是一个常量,不可以通过指针去改变一个常量的值
在这个例子之中,我们看到了顶层const和底层const同时出现在了bi这个指针之上,它代表的含义也很简单。意思就是bi这个指针是一个常量,指针所指向的变量也是一个常量。而常量的性质是不可以改变。所以bi不可以指向别的对象,也不可以通过bi去修改其指向变量的值。
总结:对于顶层const与底层const,我们需要记住,顶层const所代表的含义是指针本身是个变量,指针只能指向唯一的变量,此后不能更改,表示顶层const的方法是将const关键字写在变量类型之后,而底层const所代表的含义是指针所指向的对象是一个常量,指针本身可以指向不同的对象,但指向的对象必须是一个常量,我们不可以通过指针去修改一个常量的值,表示底层const的方法是将const关键字写在变量类型之前。
常量指针
常量指针的意思是,这个指针以为自己指向的对象是一个常量
(其实被指向的那个对象是不是常量都可以),所以自己作为一个指针不能去修改这个常量,所以叫做常量指针。
总结
指针的概念多,且杂。需要多学多用才能彻底掌握它,所以,当你忘记了一下关于指针的知识不可怕,常来复习,多学多用就好了。
不要有畏难心理!