C++指针与引用


C++的指针和引用一直是在上学期间考试的重难点,也是最难搞懂的部分。希望这次可以彻底掌握这两点。

一、指针

1.定义

上来先给出定义,定义还是比较好看懂的。

定义

指针本身就是一个变量,其符合变量定义的基本形式,它存储的是值的地址。对类型T,T*是“到T的指针”类型,一个类型为T*的变量能保存一个类型为T的对象的地址
例如int a = 122; int* b = &a,b就是一个指针变量,保存着a的地址。

间接访问

通过一个指针访问它所指向地址中的值的过程称为间接访问或者引用指针。这个用于执行间接访问的操作符是*
例如cout << (*b) << endl;

2.左值和右值

在讲解左值右值概念时先举个例子:

        int a[] = {1,2,3};
        int b[] = {4,5,6};
        a = b;

这段代码中,先把数组a初始化为{1,3,4},然后将其再指向一块为{4,5,6}的数组空间。但是,却报错了
为什么?要知道这在java中是可行的,因为java中a、b的变量名都是对一个对象的引用,所以这样的操作没有问题。但是C++却不一样。

在C++中,a代表的只是数组首元素的首地址,作用和&a是一样的。内存中并没有真的分配一块内存用来给a存储地址,所以a不能用作左值。

下面说一下左值右值的概念:

  • 左值就是编译器为其单独分配了一块存储空间,可以取其地址的。左值也可以放在运算符右边。
  • 右值指的是数据本身,不能取到自身地址。右值只能放在运算符右边。

所以,在刚刚的例子中,a = b,其实就和1 = 3一样没有意义,自然会报错。

3.几种原始指针

(1)一般类型指针 T*

可以表示任何类型的指针。比如:int*double*……

(2)指针的数组与数组的指针

指针的数组T* t[],顾名思义,就是一个数组容器,用来专门存储指针
数组的指针T(*t) [],也就是一个指向某数组的指针。

下面来看这段代码:

int main() {
    int a[] = {1,2,3,4};
    int(*b)[4] = &a;
    int* c = a;
    cout << b << " " << c << endl;
    return 0;
}

那么b和c有什么区别呢?根据上一节(左值右值)的学习,我们知道a表示的是数组首元素的首地址,而&a获取的是数组的首地址。所以打印出二者的值应该是没有区别的。
在这里插入图片描述
果然,b和c保存的地址值相等。但是!b是一个数组的指针,c是一个int型变量的指针,通过调试就可以发现它们的不同了。
在这里插入图片描述
果然和我们想的一样,其指向的内容是不同的。

(3)指向指针的指针

指向指针的指针也就是保存着一个指针的地址的变量

	int a = 123;
	int* b = &a;
	int** c = &b;

所以,*c就是间接访问b的值,**c就是间接访问a的值。

(4)未初始化和非法的指针(野指针)

首先举一个例子:

	int* a;
	*a = 12;

这肯定是不行的,因为a并没有存储一个地址,不知道a指向的是哪块内存,自然不能修改这块内存的值。
程序可以通过编译,但是不知道这块区域在哪里,所以修改会发生不可预料的问题,不能这样做。

野指针包括三种情况:没有被初始化、使用完没有置为NULL、超出变量的作用范围。

(5)NULL指针

当一个指针不指向任何东西时,可以将其赋值为NULL。

使用注意事项:

  • 创建一个指针时,如果还不知道指针该初始化为什么,就赋值为NULL,避免出现野指针;
  • 如果一个指针不用了,就将其赋值为NULL。
  • 在对一个指针进行间接引用前,先判断这个指针的值是否为NULL

4.const与指针的关系

先来看一段这样的代码:

int main() {
    char strHelloWorld[] = {"hello world"};
    char const *pStr1 = "hello world";
    char* const pStr2 = "hello world";
    char const * const pStr3 = "hello world";
    pStr1 = strHelloWorld;
    //pStr2 = strHelloWorld;
    //pStr3 = strHelloWorld;
    return 0;
}

这段代码里,定义了三个指针,在赋值操作中,pStr2与pStr3都出错了。所以const与指针搭配起来到底是个什么意思呢??

介绍一个规则,关于const修饰的部分:

  1. 看左侧最近的部分;
  2. 如果左侧没有,则看右侧。

char const *pStr1 = "hello world";为例,const左侧为char,则修饰的是char,也就是说指针的指向可变,但字符串内容不可变。
同理,char* const pStr2表示指针指向是不可变的;char const * const pStr3表示指针指向和内容均不可变。
这里还有个没有提到的,const char*char* const效果是一样的。

5.指针的基本运算

(1)&和*操作符

& 是取地址操作符,* 用来间接引用。 假设char ch = 'a'; char* cp = &ch;

  • *cp + 1表示的是先取cp地址对应的值然后加1,也就是得到'b'
  • *(cp + 1)表示的是先将cp表示的地址加1,然后再取对应地址的值。
(2)++和–操作符

还是场景char ch = 'a'; char* cp = &ch;

  • char* cp2 = ++cp cp指向的地址+1,并且cp2也指向此地址;
  • char* cp3 = cp++ cp指向的地址+1,cp3指向原来cp的地址。

其实原理和正常的++、-- 是一样的。同理*++cp*cp++的区别也显而易见了。

二、C++程序存储区域划分

观察下面一段代码,其中标注了各个变量所在内存中的区域:

int a = 0;      //(GVAR)全局初始化区
int *p1;        //(bss)全局未初始化区
int main() {
    int b = 1;          //(stack)栈区变量
    char s[] = "123";   //(stack)栈区变量
    int* p2 = NULL;     //(stack)栈区变量
    char* p3 = "123456";//"123456"在常量区,p3在栈区
    static int c = 0;   //(GVAR)全局(静态)初始化区
    p1 = new int(10);   //p1指向(heap)堆区变量
    p2 = new int(10);   //p2指向(heap)堆区变量
    char* p4 = new char[7]; //p4指向(heap)堆区变量
    strcpy_s(p4,7,"123456"); //(text)代码区
}
  • 观察栈区变量可以发现,越早定义的变量所在地址越高。因为变量存在栈中,也就是栈底是高地址,栈顶是低地址
    在这里插入图片描述
  • 观察p3的内存地址可以发现,p3定义在栈区,p3指向的字符串存放在“常量区”(因为它们的地址差太多)
    在这里插入图片描述
  • 观察p1,p2,p4发现新建出的对象都存放在堆区。堆区新建对象地址一般是从低到高。

1.内存划分概览

C++程序代码和数据在内存中的存储如图:
在这里插入图片描述

2.堆(heap)

现代编程语言大部分都是用堆来动态分配内存。动态内存会带来不确定性:内存分配耗时多久?失败了怎么办?
在C++中,new出来的对象会分配到堆,可以使用delete来释放这块内存。(java没有这样的操作,使用的是垃圾回收机制)

3.资源管理方案–RAII

RAII(Resource Acquisition Is Initialization) 依托栈和析构函数,来对所有的资源——包括堆内存在内进行管理。因为RAII的存在,使得C++不需要Java的垃圾回收机制。

RAII有些比较成熟的智能指针代表:如std::auto_ptrboost::shared_ptr

4.几块存储区域中变量对比

在这里插入图片描述
在这里插入图片描述

5.内存泄漏

(1)内存泄漏定义

指程序中已动态分配的堆内存由于某种原因程序未释放或者无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

(2)发生原因与排查方式

原因:内存泄漏主要发生在堆内存分配方式中,即“配置了内存后,所有指向该内存的指针都遗失了”。
排查:无法通过编译识别,只能在程序运行过程中来判别和判断。

其实在现在的开发中,很少让程序员自己去new和delete一块空间了。

三、智能指针

使用指针是非常危险的行为,但是指针又十分高效。所以,希望有更安全的方式来使用指针。
有两种典型的解决方案:智能指针、引用。先讲一下智能指针

C++推出了四种常用智能指针:unique_ptr、shared_ptr、weak_ptr以及已经删除的auto_ptr。

1.auto_ptr

由new expression获得对象,在auto_ptr对象销毁时,指针所指向的对象也会自动被delete掉。(但是有时候我们并不希望对象随着指针销毁而销毁)

所有权转移:如果另一个auto_ptr也指向同一个对象,那么原来的那个auto_ptr就会失去对象的控制权,原对象指针置为nullptr。在拷贝/赋值过程中,会直接剥夺对原对象的内存控制权。

int main() {
    int *pInt = new int(10);
    auto_ptr<int> pl(pInt);
    cout << *pl << endl;
    auto_ptr<int> pl2 = pl; 	//执行完这句后,pl就被剥夺了
    return 0;
}

在以上代码中,一旦第二个auto_ptr也指向同一对象,则pl将会被剥夺。
在这里插入图片描述

2.unique_ptr

unique_ptr也是保证一个对象只能被一个指针指向。但是它的实现方式是直接不支持复制和赋值
unique_ptr禁止了拷贝语义,但有时候也需要能够转移所有权,于是提供了移动语义,即可以用std::move()进行控制权转移。

int main() {
    unique_ptr<int> pl = make_unique<int>(10);
    unique_ptr<int> pl2 = move(pl);
    return 0;
}

执行move语句后可以发现,pl指向了nullptr,pl2指向原对象。
在这里插入图片描述

3.shared_ptr

shared_ptr通过一个引用计数来共享一个对象。
在垃圾标记算法中也有一个引用计数法,就是通过一个计数器来统计引用一个对象的指针个数。当计数为0时,该对象就可以被析构了。

int main() {
    auto wA = shared_ptr<int>(new int(10));
    {
        auto wA2 = wA;
        cout << ((wA.get() != nullptr)?*(wA.get()):-1) << endl;
        cout << ((wA2.get() != nullptr)?*(wA2.get()):-1) << endl;
        cout << wA.use_count() << endl;
        cout << wA2.use_count() << endl;
    }
    cout << wA.use_count() << endl;
    return 0;
}

以上代码的打印结果如下,符合定义的预期:
在这里插入图片描述

同样,shared_ptr中也有move()方法,可以将对象的指针进行转移。

循环引用问题:如果A指向B,且B指向A。那么AB就会形成一个循环,这两个对象一直不会被释放,也就会造成内存泄漏
那么如何解决这个循环引用问题呢?这便产生了新的指针weak_ptr

4.weak_ptr

weak_ptr被设计为与shared_ptr共同工作,使用观察者模式
它的作用是协助shared_ptr工作,可获得资源的观测权。观察者意味着weak_ptr只对shared_ptr进行引用,而不改变其引用计数。当被观察的shared_ptr失效后,相应的weak_ptr也相应失效。

四、引用

1.定义

引用是一种特殊的指针,是不允许修改的指针使用引用必须初始化,且一个引用永远指向它初始化的那个对象

引用创建时使用&,例如int& rx = x。引用就是变量的别名,使用时和变量名一样使用就好。当然和变量名本质区别是,引用还是个指针。

2.“交换”例子

这里就不得不拿出经典的交换例子了:

void swap_1(int a,int b){
    int temp = a;
    a = b;
    b = temp;
}
void swap_2(int& a,int& b){
    int temp = a;
    a = b;
    b = temp;
}
int main() {
    int a = 1;
    int b = 3;
    swap_1(a,b);
    cout << a << " " << b << endl;
    swap_2(a,b);
    cout << a << " " << b << endl;
    return 0;
}

输出结果为:
在这里插入图片描述

3.指针与引用的辩证关系

有了指针为什么还需要引用?
  • Bjarne Stroupstrup(C++之父)的解释:为了支持运算符重载
有了引用为什么还需要指针
  • 为了兼容C语言。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值