指针与引用

指针

  内存由很多内存单元组成。这些内存单元用于存放各种类型的数据。为了标识内存单元,计算机对内存的每个单元都进行了编号,这个编号就称为内存地址,内存地址决定了内存单元在内存中的位置。程序员并不需要记住这些内存地址,C++的编译器让我们可以通过名字来访问这些内存位置。

  指针本身就是一个变量,其符合变量定义的基本形式,它存储的值是内存地址。对于一个基本类型T,T* 是“到T的指针”类型,一个类型为T*的变量能够保存一个类型为T的对象的地址。

  通过一个指针访问它所指向的地址的过程称为间接访问或者引用指针。这个用于执行间接访问的操作符是单目操作符*。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main()
{
int a = 1;
float b = 3.14;
//指针定义
int* c = &a; //c存储a的内存地址
float* d = &b;

cout << c << endl; //输出a变量的地址

//间接访问
cout << (*c) << endl; //等同于cout<<a<<endl;
cout << (*d) << endl;//等同于cout<<b<<endl;

return 0;
}

左值与右值

字符串本身就是一个字符数组,但是字符串还可以用指针来表示:

1
2
3
4
5
6
7
8
9
10
int main()
{
//两种字符串表示
char strHello[] = { "hello" };
const char* pStrHello = "hello";

pStrHello = strHello; //正确,指针变量的值可以改变
strHello = pStrHello; //错误,数组变量的值不允许改变
return 0;
}
strHello不可改变,strHello[index]的值可以改变 pStrHello可以改变,pStrHello[index]的值能否改变取决于所指的存储区域是否可变 这里就涉及到了左值与右值的概念

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

左值最常见的情况就是函数和数据成员的名字。 右值是没有标识符、不可取地址的表达式,一般也称为“临时对象”。

a = b + c; &a是允许的操作,a是一个左值 &(b+c)不能通过编译,b+c是一个右值

C++原始指针

一般类型指针T*

T是一个泛型,泛指任何一种类型

1
2
3
4
5
6
7
int i = 4;
int* iP = &i;
cout<<(*iP)<<endl;

double d = 3.14;
double* dP =&d;
cout<<(*dP)<<endl;

不论T是什么类型,T*这个指针的内存空间都是一样的,为4个字节

指针的数组 与 数组的指针

指针的数组 T*t[]:指针的数组仍然是数组,里面每个值是个指针(arrao of pointers) 数组的指针 T(*t)[] :一个指针,指向一个数组(a pointer to an array)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int* a[4];

int c[4] = {1,2,3,4};
int(*b)[4];
b = &c; //数组的个数一定要匹配
//注意,[]的优先级比较高


for(unsigned int i = 0;i<4;i++)
{
a[i] = &(c[i]);
}



cout<<*(a[0])<<endl; //先取数组下标得到地址,然后做间接访问
cout<<(*b)[3]<<endl; //b是指针,先间接访问取值得到数组,然后取数组下标

const pointer && pointer to const

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main()
{
char strHello[] = { "helloworld" };

char const* pStr1 = "helloworld"; //修饰char,指针指向的地址可变,但是存储的区域中的内容不可变
char* const pStr2 = "helloworld"; //修饰char*,指针指向的地址不可变。在最新的VS中编译不过,因为"helloworld"是常量,必须要用const指针
char const* const pStr3 = "helloworld"; //地址和空间中的内容都不允许改变

pStr1 = strHello;
//pStr2 = strHello; //错误,pStr2地址不可改
//pStr3 = strHello; //错误,pStr3地址不可改

//pStr1[1] = 'a'; //错误,存储区内的char不可改变
pStr2[1] = 'a';
//pStr3[1] = 'a'; //错误,存储区内的char不可变

return 0;
}

关于const修饰:

  • 看左侧最近的部分
  • 如果左侧没有,则看右侧

pointer to pointer

1
2
3
int a = 123;
int* b = &a;
int** c = &b;

*操作符具有从右向左的结合性,**c 这个表达式相当于 *(*c),必须从里向外逐层求值 *c得到的是c指向的位置,即b **c相当于 *b,得到变量a的值

表达式
a 123
b &a
*b a,123
c &b
*c b,&a
**c *b,a,123

未初始化指针和非法指针

1
2
int* a;
*a = 12;//错误!!

  上述操作并没有对指针a进行初始化,也就是说我们并不知道a最终会指向哪里。运气好的话定位到一个非法地址(程序不能访问的地址),程序会出错从而终止。最坏的情况下,a定位到了一个可以访问的地址,这样我们就无意间修改了它,这样的错误难以捕捉,引发的错误与原先用来操作的代码毫不相干,我们根本无法定位。

用指针进行间接访问之前,一定要确保它已经初始化,并且被恰当的赋值。

NULL指针

NULL指针是一个特殊的指针变量,表示不指向任何东西。

1
int* a = NULL;

NULL指针的概念非常有用,它给了一种方法,来表示特定的指针目前未指向任何东西。

对于一个指针,如果已经知道将被初始化为什么地址,那么请给他赋值,否则请把它设置为NULL,这样可以有效避免不可确定性访问的问题。

在对一个指针间接引用前,先判断这个指针的值是否为NULL。

指针使用完成后也请重新赋值为NULL。

野指针

野指针是指向“垃圾”内存的指针。if等判断对它们不起作用,因为没有置为NULL,它存有值,但是我们用不了;

一般情况下有三种情况被称为野指针 1. 指针变量没有初始化 2. 已经释放不用的指针没有置为NULL,如delete和free之后的指针 3. 指针操作超越了变量的作用域范围(指针指向具有一定生命周期的空间)

没有初始化的,不用的或者超出范围的指针,请一定置为NULL

指针的基本运算

1
2
char ch = 'a';
char* cP = &ch;

&操作符不能做左值,&操作编译器做是事情是把变量的地址位置取出来,然后放在内存空间中。但是他本身并不是变量自身,仅仅是一块空间存储着变量地址,这块空间我们的程序是没办法获取到的。

间接引用操作当用作左值的时候,实际的操作是把变量ch当前的位置取出来(取空间),这种操作我们可以对这块空间进行操作,比如赋值操作。 当我们把他当作右值时,实际的操作取的就不是存储空间,而是存储空间中的值。

*cp + 1首先得到cp中的值,得到a,做+1操作就是对ASCII码进行操作,得到b。但是这个操作还是由编译器创造一块空间取值,我们得不到这个变量的地址,不能做左值。这个+1的操作是按照cp的类型来做加法的,移动的是cp这个类型的大小。

*(cp+1)操作我们先做了+1,而cp本身是个指针,我们做的是指针的加法,得到的是ch这个变量的地址的后面那个地址(做这个操作前要确定cp指向的地址后面的内容是可以访问的)。这个操作也是可以用作左值和右值,左值就是取地址,右值就是取空间中存储的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main()
{
char ch = 'a';
//&操作符
//&ch = 97; //错误,&ch左值不合法
char* cp = &ch; //&ch右值
//&cp = 97; //错误,&左值不合法
char** cpp = &cp; //&cp右值


//*操作符
*cp = 'a'; //*cp左值取变量ch的位置
char ch2 = *cp; //*cp右值取变量ch存储的值
//*cp + 1 = 'a'; //错误,*cp+1左值不合法的位置
ch2 = *cp + 1; //*cp+1右值取到的字符做ASCII码+1操作
*(cp + 1) = 'a'; //左值,语法上合法,访问到cp后面的位置,赋值为a.一定要保证这个位置是可以访问的,这种操作有风险
ch2 = *(cp + 1); //右值操作,取ch后面的位置的值
}

指针的++ 与 --

1
2
3
4
5
6
7
char* cp2 = ++cp;
//汇编代码:
mov eax,dword ptr [cp] //eax是寄存器,dwptr存储cp指针。把指针内容放置寄存器内
add eax,1 //寄存器数据+1
mov dword ptr [cp],eax //把寄存器内容存回cp中
mov ecx,dword ptr [cp] //把cp的内容放置在ecx寄存器
mov dword ptr [cp2],ecx //把寄存器ecx内容放置在cp2中
1
2
3
4
5
6
7
char* cp3 = cp++;
//汇编代码:
mov eax,dword ptr[cp] //把cp指针内容放置在eax寄存器中
mov dword ptr[cp3],eax //把eax内容直接放在cp3指针
mov ecx,dword ptr[cp] //把cp信息放置在exc寄存器中
add ecx,1 //ecx+1操作
mov dword ptr[cp],ecx //ecx内容写入cp指针

前置操作先做加法再赋值,后置操作先赋值后做加法操作。 自减操作符和自增操作符相同,前置操作先做减法再赋值,后置操作先赋值再做减法。

自增自减操作获得的地址不能当作左值,它只是个地址的副本,没有明确存储的位置。

++操作符优先级高于*

++++和----等运算符

  编译器程序分解符号的方法是:一个字符一个字符的读入,如果该字符可能组成一个符号,那么读入下一个字符,一直到读入的字符不能组成一个有意义的符号。这个处理过程称为“贪心法”。

1
2
3
4
5
int a = 1,b=2
int c;
int d;
c = a+++b; //相当于a++ +b。连续读取,当读取到两个+号时不能再组成新符号了
d = a++++b; //相当于a++ ++b。这个是错误的 ,不构成任何运算

C++程序的存储区域划分

栈和队列

  数据结构中有两种常见的结构,一种是栈结构,先进入的数据会被压在栈底,后进入的数据会被放在栈顶。是一种先进后出的结构;还有一种是队列结构,和栈相反,类似于生活中的队列,先进入的数据会先出队列。在C++中,栈是一种很常见的结构,我们一般性的变量都在栈上,函数也会在栈上处理。

存储区域划分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <iostream>
using namespace std;

int a = 0; //GVAR全局初始化区
int* p1; //BSS全局未初始化区

int main() //text 代码区
{
int b = 1; //stack栈区
char s[] = "abc"; //stack栈区
int* p2 = NULL; //stack栈区

const char* p3 = "123456"; //"123456\0"在常量区,p3在stack栈区

static int c = 0; //GVAR全局(静态)初始化区

p1 = new int(10); //heap堆区变量
p2 = new int(20); //heap堆区变量

char* p4 = new char[7]; //heap堆区变量
strcpy_s(p4, 7, "123456"); //text代码区

if (p1 != NULL)
{
delete p1;
p1 = NULL;
}

if (p2 != NULL)
{
delete p2;
p2 = NULL;
}

//p3指向常量区,由内存接管

if (p4 != NULL)
{
delete[] p4;
p4 = NULL;
}

return 0;
}

通过调试上面的代码,可以观察到一些程序中的地址分布:

上图是栈区变量b,s,p2的地址空间,可以看到虽然我们定义变量的顺序是b,s,p2,但是内存空间的地址位置是从高地址到低地址变化的,越早分配的变量,拿到的地址位置越高。

再观察p3变量。看p3本身的地址可以观察到它的地址分配再p2的上面,因为都是栈区变量,但是内部存储的一个地址并不是在栈区,是在常量区。在常量区中的内容,我们是无法修改的。但是如果p2指向的是一个字符数组,那么指向的就是栈区空间,是可以改变的。

继续观察p1,p1是在函数之外声明的,看地址也可以观察到,它的地址和b,s相差很大,可知它并不在栈区。这种定义在函数外的变量属于全局的区域。

当p1和p2执行完new操作后,观察p1和p2指向的地址空间,发现两个区域相邻。而且p1先new,p2后new,地址空间p2指向的地址也比p1要高。new操作会产生新的区域,我们称为堆区,和栈区相反,内存分配方式由低地址向高地址分配。

再看p4,p4本身是在main函数中定义的,是栈区变量。它new的是一个char型的数组,new出的地址和p1和p2指向的地址也很接近,可知也是堆区内。

对存储区域做一个总结,如下图:

动态分配资源--堆区

  1. 从现代编程语言的观点来看,使用堆,或者说使用动态内存分配,是一件很自然的事情;
  2. 动态内存带来了不确定性:内存分配耗时需要多久(分配大空间不好控制)?失败了怎么办?在实时性要求很高的场合,入嵌入式设备,这些不确定性是很严重的;
  3. 一般而言,当我们在堆上分配内存时,很多语言会使用new这样的关键字,也有些语言是隐式分配,不使用new的语义,但使用的是new的方式。在C++中new对应词是delete,因为C++是允许程序员完全接管内存的分配释放的。

分配和回收动态内存的原则

程序通常需要牵扯到三个内存管理器的操作:

  1. 分配一个某大小的内存块;
  2. 释放一个之前分配的内存块;
  3. 垃圾收集操作,寻找不再使用的内存块并给予释放;

这个回收策略需要实现性能、实时性、额外开销等各方面的平衡,很难有统一和高效的做法。 C++使用了1和2;Java使用了1和3。

资源管理方案--RAII(Resource Acquisition Is Initization)

  • C++特有的资源管理方式,主流的编程语言中,C++是唯一一个依赖RAII来做资源管理的,核心思想是分配资源的时候就可以管理资源;
  • RAII依托析构函数,来对所有的资源--包括堆内存在内进行管理。比如一个对象在构造和析构中就把资源管理起来,当对象生存空间超出后进入析构状态,我们就可以进行资源的释放。堆RAII的使用,使得C++不需要类似于Java哪样的垃圾收集方法也能有效管理内存。
  • RAII有些比较成熟的智能指针代表,如std::auto_ptr和boost::stared_ptr。

C++的几种变量的对比

stack heap
作用域 函数体内,语句块{}作用域,超出后被系统回收 整个程序范围内,由new、malloc开始,delete、free结束
编译期间大小确定 变量大小范围确定 需要运行期间才能确定
大小范围 Win默认1M,Linux默认8M或10M,注意空间很小,不要分配大内存变量 所有系统的堆空间上限接近内存(虚拟内存)总大小(有一部分被OS占用)
内存分配方式 地址由高到底减少 地址由低到高增加
内容是否可变 可变 可变
全局静态存储区 常量存储区
存储内容 全局变量,静态变量 常量
编译期间大小是否确定 确定 确定
内容是否可变 可变 不可变

内存泄漏(Memory Leak)问题

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

内存泄漏发生的原因和排查方式:

  1. 内存泄漏主要发生在堆内存分配方式中,即“配置了内存后,所有指向该内存的指针都遗失了”。如果缺乏垃圾回收机制,这样的内存片就无法归还系统;
  2. 因为内存泄漏属于程序运行中的问题,无法通过编译识别,所以只能在程序运行过程中来判别和诊断。

比指针更安全的解决方案

  使用指针是非常危险的行为,可能存在空指针,野指针的问题,并可能造成内存泄漏问题。可是指针又非常高效,所以我们希望以更安全的方式来使用指针。一般有两种典型方案:

  • 使用更安全的指针:智能指针;
  • 使用安全的方式:引用;

C++的智能指针

  C++推出了四种常见的智能指针:unique_ptr、shared_ptr、weak_ptr和C++11中已经废弃(deprecated)的auto+ptr,C++17中auto+ptr已经被正式删除。

auto_ptr

  auto_ptr是一种简单直接的智能指针,可以指向一个泛型对象。我们由new获得的对象在堆区中,如果auto_ptr指向这个对象,那么在auto_ptr对象销毁的时候,它所管理的对象也会一并delete掉,这不是一个特别合理的行为,因为指针指向对象不是一种强关联的关系。   auto_ptr还要注意所有权转移的问题:有一个auto_ptr指向一个对象,如果我们不小心把对象传递给另外的智能指针,原来的指针就不再拥有这个对象了。这个操作是通过C++中的拷贝构造和赋值完成的,会直接剥夺指针对源对象内存的控制权。被剥夺后,对象内存的所有权转移给新指针,然后将原对象指针置为nullptr。因为这个问题,导致auto_ptr存在很大的安全隐患,这是被废弃的重要原因。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <string>
#include <iostream>
#include <memory> //智能指针头文件
using namespace std;
int main()
{
{// 确定auto_ptr失效的范围
// 对int使用
auto_ptr<int> pI(new int(10)); //关注pI的生命周期范围和指向的堆区空间的声明周期范围
cout << *pI << endl; // 10

// auto_ptr C++ 17中移除 拥有严格对象所有权语义的智能指针
// auto_ptr原理:在拷贝 / 赋值过程中,直接剥夺原对象对内存的控制权,转交给新对象,
// 然后再将原对象指针置为nullptr(早期:NULL)。这种做法也叫管理权转移。
// 他的缺点不言而喻,当我们再次去访问原对象时,程序就会报错,所以auto_ptr可以说实现的不好,
// 很多企业在其库内也是要求不准使用auto_ptr。

//对字符串数组使用
auto_ptr<string> languages[5] = {
auto_ptr<string>(new string("C")),
auto_ptr<string>(new string("Java")),
auto_ptr<string>(new string("C++")),
auto_ptr<string>(new string("Python")),
auto_ptr<string>(new string("Rust"))
};
cout << "There are some computer languages here first time: \n";
for (int i = 0; i < 5; ++i)
{
cout << *languages[i] << endl;
}
auto_ptr<string> pC;
pC = languages[2]; // languges[2] loses ownership. 将所有权从languges[2]转让给pC!!!
//此时languges[2]不再引用该字符串从而变成空指针
cout << "There are some computer languages here second time: \n";
for (int i = 0; i < 2; ++i)
{
cout << *languages[i] << endl;
}
cout << "The winner is " << *pC << endl;
//下面会报错
//cout << "There are some computer languages here third time: \n";
//for (int i = 0; i < 5; ++i)
//{
// cout << *languages[i] << endl;
//}
}
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//memory文件

//观察auto_ptr的模板可以看到它是怎么实现资源释放和所有权转移的

//资源释放:是通过析构来完成的
~auto_ptr() noexcept {
delete _Myptr;
}


//所有权转移,观察赋值的重载:
auto_ptr& operator=(auto_ptr& _Right) noexcept {
reset(_Right.release()); //保存原有的地址,把原有保存这个地址的指针置成Nullptr
return *this;
}

unique_ptr

  auto_ptr提供了自动管理内存的一个方法,但是它和对象的耦合性太紧了,如果多方操作对象很容易出问题,所以推出了unique_ptr。unique_ptr是专属所有权,所以被unique_ptr管理的内存,只能被一个对象持有,不持支复制和赋值。   移动语义:虽然unique_ptr禁止了拷贝语义,但有时候我们也需要能够转移所有权,于是提供了移动语义,即可以使用std::move()进行所有权的转移。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <memory>
#include <iostream>
using namespace std;
int main()
{
// 在这个范围之外,unique_ptr被释放
{
auto i = unique_ptr<int>(new int(10));
cout << *i << endl;
}

// unique_ptr
auto w = std::make_unique<int>(10);
cout << *(w.get()) << endl; // 10。get()方法返回的就是指针

//auto w2 = w; // 编译错误!!如果想要把 w 复制给 w2, 是不可以的。
// 因为复制从语义上来说,两个对象将共享同一块内存。

// unique_ptr 只支持移动语义, 即如下
auto w2 = std::move(w); // w2 获得内存所有权,w 此时等于 nullptr
cout << ((w.get() != nullptr) ? (*w.get()) : -1) << endl; // -1
cout << ((w2.get() != nullptr) ? (*w2.get()) : -1) << endl; // 10
return 0;
}

shared_ptr和weak_ptr

  unique_ptr在同一时间只能由一个指针持有对象,使用上具有局限性。所以推出了shared_ptr。   shared_ptr通过一个引用计数共享一个对象,在这个机制上提供了可以共享所有权的智能指针,当然这需要额外的开销。当引用计数为0时,说明该对象没有被使用,可以进行析构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <iostream>
#include <memory>
using namespace std;
int main()
{
// shared_ptr
{
//shared_ptr 代表的是共享所有权,即多个 shared_ptr 可以共享同一块内存。
auto wA = shared_ptr<int>(new int(20));
{
auto wA2 = wA; //shared_ptr可以赋值,同时指向对象
cout << ((wA2.get() != nullptr) ? (*wA2.get()) : -1) << endl; // 20
cout << ((wA.get() != nullptr) ? (*wA.get()) : -1) << endl; // 20
cout << wA2.use_count() << endl; // 打印引用计数 2
cout << wA.use_count() << endl; // 打印引用计数 2
}
//cout << wA2.use_count() << endl;
cout << wA.use_count() << endl; // wA2消亡,引用计数为1
cout << ((wA.get() != nullptr) ? (*wA.get()) : -1) << endl; // 20
//shared_ptr 内部是利用引用计数来实现内存的自动管理,每当复制一个 shared_ptr,
// 引用计数会 + 1。当一个 shared_ptr 离开作用域时,引用计数会 - 1。
// 当引用计数为 0 的时候,则 delete 内存。
}
//跳出作用域,wA也消亡,同时内存会被释放

// shared_ptr也支持move()语法
auto wAA = std::make_shared<int>(30);
auto wAA2 = std::move(wAA); // 此时 wAA 等于 nullptr,wAA2.use_count() 等于 1
cout << ((wAA.get() != nullptr) ? (*wAA.get()) : -1) << endl; // -1
cout << ((wAA2.get() != nullptr) ? (*wAA2.get()) : -1) << endl; // 30
cout << wAA.use_count() << endl; // 0
cout << wAA2.use_count() << endl; // 1
//将 wAA 对象 move 给 wAA2,意味着 wAA 放弃了对内存的所有权和管理,此时 wAA对象等于 nullptr。
//而 wAA2 获得了对象所有权,但因为此时 wAA 已不再持有对象,因此 wAA2 的引用计数为 1。

return 0;
}

  引用计数也会带来一个严重问题:循环引用。即存在一种情况,有两个对象,对象A内部有shared_ptr指针指向B,B中也有shared_ptr指向A,那么循环引用会导致堆里面的内存无法正常回收,造成内存泄漏。

  为了避免这种循环引用,标准库提供了weak_ptr,被用来和shared_ptr共同工作,用一种观察者模式工作。比如两个对象A和B互为关联,但B只是想获取A的一些属性,并不需要A的所有权,那么可以用weak_ptr,指向A但是并不拿A的引用计数。因为B没有A的引用计数,那么A销毁的时候,B也可以同时销毁。这就是观察者模式,观察者意味着weak_ptr只对shared_ptr进行引用,而不改变其引用计数,当被观察的shared_ptr失效后,相应的weak_ptr也失效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <string>
#include <iostream>
#include <memory>
using namespace std;

struct B;
struct A {
shared_ptr<B> pb;
~A()
{
cout << "~A()" << endl;
}
};
struct B {
shared_ptr<A> pa;
~B()
{
cout << "~B()" << endl;
}
};

// pa 和 pb 存在着循环引用,根据 shared_ptr 引用计数的原理,pa 和 pb 都无法被正常的释放。
// weak_ptr 是为了解决 shared_ptr 双向引用的问题。
struct BW;
struct AW
{
shared_ptr<BW> pb;
~AW()
{
cout << "~AW()" << endl;
}
};
struct BW
{
weak_ptr<AW> pa;
~BW()
{
cout << "~BW()" << endl;
}
};

void Test()
{
cout << "Test shared_ptr and shared_ptr: " << endl;
shared_ptr<A> tA(new A());
shared_ptr<B> tB(new B());
cout << tA.use_count() << endl; //1
cout << tB.use_count() << endl; //1
tA->pb = tB;
tB->pa = tA;
cout << tA.use_count() << endl; // 2
cout << tB.use_count() << endl; // 2
}
void Test2()
{
cout << "Test weak_ptr and shared_ptr: " << endl;
shared_ptr<AW> tA(new AW());
shared_ptr<BW> tB(new BW());
cout << tA.use_count() << endl; // 1
cout << tB.use_count() << endl; // 1
tA->pb = tB;
tB->pa = tA; //weak_ptr不会对tA的引用计数产生影响
cout << tA.use_count() << endl; // 1
cout << tB.use_count() << endl; // 2
}

int main()
{
Test();
Test2();


return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
上面代码输出:
Test shared_ptr and shared_ptr:
1
1
2
2
Test weak_ptr and shared_ptr:
1
1
1
2
~AW()
~BW()


可以看到weak_ptr不会对引用计数产生影响,而产生循环引用的地方不会发生析构

引用

引用在本质上仍然是是指针,只不过自身比较特殊,是不允许修改的指针。(我们常说java中没有指针,其实java中的指针就是引用)

在指针使用上,我们会遇到一些问题:

  1. 空指针
  2. 野指针
  3. 不知不觉改变了指针的值,我们却仍然在使用

使用引用,我们可以避免这些问题:

  1. 不存在空引用;
  2. 引用必须被初始化;
  3. 一个引用永远指向它初始化的那个对象,不允许被修改。

引用可以认为是指定变量的别名,使用时可以认为是变量本身:

1
2
3
4
5
6
int x1 = 1,x2 = 3;
int& rx = x1; //定义引用,可以认为rx是x1的别名
rx = 2;
cout<<x1<<rx<<endl; //x1和rx都是2
rx = x2; //引用一旦被初始化就不能更改,所以这里不是赋值rx为x2,而是x1=x2(别名直接替换)
cout<<x1<<x2<<endl; //都是3

当我们在函数中需要操作形参并且返回时一并返回,这时候我们就可以传递引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
#include <assert.h>
using namespace std;

// 编写一个函数,输入两个int型变量a,b
// 实现在函数内部将a,b的值进行交换。
void swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
void swap2(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}

int main()
{
// 交换变量的测试
int a = 3, b = 4;
swap(a, b);
assert(a == 4 && b == 3);

a = 3, b = 4;
swap2(&a, &b);
assert(a == 4 && b == 3);

return 0;
}

  C++为什么要同时存在指针和引用?在java语言中我们直接使用引用,传统C语言我们都使用指针。C++可以认为是夹在C和java之间的一种。之所以要使用引用是为了支持函数的运算符重载。而C++为了兼容C语言不能摒弃指针。

  在函数传递参数的时候,对于内置基础类型(int、double等)而言,在函数中传递值更高效(pass by value);在面向对象中自定义类型而言,在函数中传递const引用更高效(pass by reference to const)。