指针
内存由很多内存单元组成。这些内存单元用于存放各种类型的数据。为了标识内存单元,计算机对内存的每个单元都进行了编号,这个编号就称为内存地址 ,内存地址决定了内存单元在内存中的位置。程序员并不需要记住这些内存地址,C++的编译器让我们可以通过名字来访问这些内存位置。
指针本身就是一个变量,其符合变量定义的基本形式,它存储的值是内存地址。对于一个基本类型T,T*
是“到T的指针”类型,一个类型为T*的变量能够保存一个类型为T的对象的地址。
通过一个指针访问它所指向的地址的过程称为间接访问 (indirection)或者引用指针 (dereferencing
the point)。这个用于执行间接访问的操作符是单目操作符*。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 int main () { int a = 1 ; float b = 3.14f ; int * c = &a; float * d = &b; cout << c << endl; cout << (*c) << endl; cout << (*d) << 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是允许的操作而&(b+c)不能通过编译,因此a是一个左值,(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[]:指针的数组仍然是数组,里面每个值是个指针(array 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 18 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]); } int *p = a; cout<<*(a[0 ])<<endl; cout<<(*b)[3 ]<<endl;
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 * const pStr2 = "helloworld" ; const char * const pStr3 = "helloworld" ; pStr1 = strHello; pStr2[1 ] = 'a' ; return 0 ; }
如何确定const修饰的内容:
指向指针的指针
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
未初始化指针和非法指针
上述操作并没有对指针a进行初始化,也就是说我们并不知道a最终会指向哪里。运气好的话定位到一个非法地址(程序不能访问的地址),程序会出错从而崩溃终止。最坏的情况下,a定位到了一个可以访问的地址,这样我们就无意间修改了它,这样的错误难以捕捉,引发的错误与原先用来操作的代码毫不相干,我们根本无法定位。
用指针进行间接访问之前,一定要确保它已经初始化,并且被恰当的赋值。
NULL、nullptr和void*
NULL指针是一个特殊的指针变量,表示不指向任何东西。
NULL指针的概念非常有用,它给了一种方法,来表示特定的指针目前未指向任何东西。
对于一个指针,如果已经知道将被初始化为什么地址,那么请给他赋值,否则请把它设置为NULL,这样可以有效避免不可确定性访问的问题;
在对一个指针间接引用前,先判断这个指针的值是否为NULL;
指针使用完成后也请重新赋值为NULL;
在早期的C语言中,编译器定义:#define NULL
((void*)0) 。void*是一个很万能的指针,可以转换任意的类型。参数传递时经常用到,函数编写时无法预测将来要传递什么信息,就用void*代替。
C++语言诞生时没有把NULL定义为void*,而是定义为了0:
1 2 3 4 5 6 7 #ifndef NULL #ifdef __cplusplus #define NULL 0 #else #define NULL ((void*)0) #endif #endif
C++中NULL表示为int而不是指针,和C语言的差异会导致问题,所以C++11中用nullptr来代替(void*)0,NULL只表示0。在新的C++标准中,空指针尽量使用nullptr来表示。
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 void func (void * i) { cout << "func(void* i)" << endl; } void func (int i) { cout << "func(int i)" << endl; } int main () { int * pi = NULL ; int * pi2 = nullptr ; char * pc = NULL ; char * pc2 = nullptr ; func (NULL ); func (nullptr ); func (pi); func (pi2); func (pc); func (pc2); return 0 ; }
函数指针
函数指针是指指向函数而非指向对象的指针。像其他指针一样,函数指针也指向某个特定的函数类型,函数类型由其返回类型以及形参列表确定,与函数名无关:
1 2 3 4 5 bool (*pf)(const string&, const string&)
函数指针类型相当冗长。使用typedef为指针类型定义同义词,可以将函数指针的使用大大简化。对函数指针初始化时,可以直接使用函数名。直接引用函数名等效于在函数名上应用取地址操作符。指向函数的指针可用于调用它所指向的函数。
1 2 3 4 5 6 7 8 9 10 11 typedef bool (*cmpFcn) (const string&, const string&) ;bool lengthCompare (const string&, const string&) ;cmpFun pf = lengthCompare; lengthCompare ("h1" , "bye" ); pf ("h1" , "bye" ); (*pf)("h1" , "bye" );
函数的形参也可以是指向函数的指针,可以用以下两种方式编写:
1 2 void useBigger (const string&, const string&, bool (const string&,const string&)) ;void useBigger (const string&, const string&, bool (*)(const string&,const string&)) ;
函数的返回值也可以返回指向函数的指针,但是写出这种返回类型相当不容易:
1 2 3 4 5 6 7 8 9 int (*ff (int ))(int *, int );typedef int (*PF) (int *, int ) ;PF ff (int ) ;
野指针
野指针是指向“垃圾”内存的指针。if等判断对它们不起作用,因为没有置为NULL,它存有值,但是我们用不了;
一般情况下有三种情况被称为野指针: 1. 指针变量没有初始化; 2.
已经释放不用的指针没有置为NULL,如delete和free之后的指针; 3.
指针操作超越了变量的作用域范围(指针指向具有一定生命周期的空间);
没有初始化的,不用的或者超出范围的指针,请一定置为NULL
指针的基本运算
C/C++常常把地址当作整数处理,但不意味着程序员可以对地址进行各种算术操作。指针可以做的运算有限,像指针与其他变量的乘除、两个指针之间的乘除、两个指针相加都是没有意义、不被编译器接受的。
算数运算
指针加上一个整数的结果是另一个指针。当一个指针和一个整数量进行算术运算时,整数在执行加法运算前始终会根据合适的大小进行调整,这个“合适的大小”指的是指针所指向的类型的大小。假设某台机器float占据4字节,则float类型指针+3时,这个3将根据float的大小变为12,即指针增加3个floag的大小。
也可以进行指针之间的算数运算,但只有两个指针指向同一个数组中的元素时才允许这种运算,如果不是同一个数组中的元素,相减的结果是未定义的。减法的运算表示的是两个指针在内存中的距离,以数组元素的长度为单位,而不是以字节为单位
&与*操作符
1 2 char ch = 'a' ;char * cP = &ch;
&操作符不能做左值,&操作编译器做是事情是把变量的地址位置取出来,然后放在内存空间中。但是他本身并不是变量自身,仅仅是一块空间存储着变量地址,这块空间的地址我们的程序是没办法获取到的。就像上图,&ch操作拿到的是ch变量的地址,但取出的信息不会像cp这个变量一样有一块能获取地址的内存空间来存储。一定要注意,虽然cp是ch的地址,&ch也是ch的地址,但这俩不是一个概念,只是恰好存的东西一样。
1 2 3 *cp = 'b' ; char tmp = *cp;
间接引用操作当用作左值的时候,实际的操作是把变量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 19 int main () { char ch = 'a' ; char * cp = &ch; char ** cpp = &cp; *cp = 'a' ; char ch2 = *cp; ch2 = *cp + 1 ; *(cp + 1 ) = 'a' ; ch2 = *(cp + 1 ); }
指针的++ 与 --
1 2 3 4 5 6 7 char * cp2 = ++cp;mov eax,dword ptr [cp] add eax,1 mov dword ptr [cp],eax mov ecx,dword ptr [cp] mov dword ptr [cp2],ecx
1 2 3 4 5 6 7 char * cp3 = cp++;mov eax,dword ptr[cp] mov dword ptr[cp3],eax mov ecx,dword ptr[cp] add ecx,1 mov dword ptr[cp],ecx
前置操作先做加法再赋值,后置操作先赋值后做加法操作。自减操作符和自增操作符相同,前置操作先做减法再赋值,后置操作先赋值再做减法。
自增/自减操作获得的地址不能当作左值,它得到的只是个地址的副本,没有明确的变量来存储它的位置。
++操作符优先级高于*,先计算地址偏移;
++++和----等运算符连续
编译器程序分解符号的方法是:一个字符一个字符的读入,如果该字符可能组成一个符号,那么读入下一个字符,一直到读入的字符不能组成一个有意义的符号。这个处理过程称为“贪心法”。
1 2 3 4 5 int a = 1 ,b=2 ;int c;int d;c = a+++b; d = 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 45 46 47 48 49 #include <iostream> using namespace std;int a = 0 ; int * p1; int main () { int b = 1 ; char s[] = "abc" ; int * p2 = NULL ; const char * p3 = "123456" ; static int c = 0 ; p1 = new int (10 ); p2 = new int (20 ); char * p4 = new char [7 ]; strcpy_s (p4, 7 , "123456" ); if (p1 != NULL ) { delete p1; p1 = NULL ; } if (p2 != NULL ) { delete p2; p2 = NULL ; } 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指向的地址也很接近,可知也是堆区内。
对存储区域做一个总结,如下图:
动态分配资源--堆区
从现代编程语言的观点来看,使用堆,或者说使用动态内存分配,是一件很自然的事情;
动态内存带来了不确定性:内存分配耗时需要多久(分配大空间不好控制)?分配失败了怎么办?在实时性要求很高的场合,如嵌入式控制器和电信设备,这些不确定性是很严重的;
一般而言,当我们在堆上分配内存时,很多语言会使用new这样的关键字,也有些语言是隐式分配,不使用new的语义,但使用的是new的方式。在C++中new对应词是delete,因为C++是允许程序员完全接管内存的分配释放的。
分配和回收动态内存的原则
程序通常需要牵扯到三个内存管理器的操作:
分配一个某大小的内存块;
释放一个之前分配的内存块;
垃圾收集操作,寻找不再使用的内存块并给予释放;
这个回收策略需要实现性能、实时性、额外开销等各方面的平衡,很难有统一和高效的做法。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++的几种变量的对比
作用域
函数体内,语句块{}作用域,超出后被系统回收
整个程序范围内,由new、malloc开始,delete、free结束
编译期间大小确定
变量大小范围确定
需要运行期间才能确定
大小范围
Windows默认1M,Linux默认8M或10M,注意空间很小,不要分配大内存变量
所有系统的堆空间上限接近内存(虚拟内存)总大小(有一部分被OS占用)
内存分配方式
地址由高到底减少
地址由低到高增加
内容是否可变
可变
可变
存储内容
全局变量,静态变量
常量
编译期间大小是否确定
确定
确定
内容是否可变
可变
不可变
内存泄漏(Memory Leak)问题
内存泄漏指的是程序中已经动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果;
内存泄漏主要发生在堆内存分配方式中,即“配置了内存后,所有指向该内存的指针都遗失了”。如果缺乏垃圾回收机制,这样的内存片就无法归还系统;
因为内存泄漏属于程序运行中的问题,无法通过编译识别,所以只能在程序运行过程中来判别和诊断。
比指针更安全的解决方案
使用指针是非常危险的行为,可能存在空指针,野指针的问题,并可能造成内存泄漏问题。可是指针又非常高效,所以我们希望以更安全的方式来使用指针。一般有两种典型方案:
使用更安全的指针:智能指针;
不使用指针,使用更安全的方式:引用;
C++的智能指针
C++推出了四种常见的智能指针:unique_ptr、shared_ptr、weak_ptr和C++11中已经废弃(deprecated)的auto+ptr,C++17中auto_ptr已经被正式删除。
auto_ptr
auto_ptr是一种简单直接的智能指针,可以指向一个泛型对象。我们由new获得的对象在堆区中,这不是一个特别合理的行为,因为指针指向对象不是一种强关联的关系。
需要注意,auto_ptr的析构函数中删除指针用的是delete而不是delete[],所以不应该用auto_ptr来管理指针数组。同时因为STL标准容器需要用到大量的拷贝赋值操作,auto_ptr也不能用于STL中。
所有权转移:有一个auto_ptr指向一个对象,如果我们不小心把对象传递给另外的智能指针(即有另一个auto_ptr指向了原来的对象),原来的指针就不再拥有这个对象了。这个操作是通过C++中的拷贝构造和赋值完成的,会直接剥夺指针对源对象内存的控制权。被剥夺后,对象内存的所有权转移给新指针,然后将原对象指针置为nullptr。这个问题影响很大,如果将auto_ptr作为函数参数按值传递,因为函数在调用过程中会产生一个局部对象接收传入的auto_ptr(拷贝构造),会导致传入的实参失去对原有对象的控制权,原对象在函数退出后被局部auto_ptr删除。因为这个问题,导致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 49 50 #include <string> #include <iostream> #include <memory> using namespace std;int main () { { auto_ptr<int > pI (new int (10 )) ; cout << *pI << endl; 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 ]; 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; } return 0 ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ~auto_ptr () noexcept { delete _Myptr; } auto_ptr& operator =(auto_ptr& _Right) noexcept { reset (_Right.release ()); 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 25 #include <memory> #include <iostream> using namespace std;int main () { { auto i = unique_ptr <int >(new int (10 )); cout << *i << endl; } auto w = std::make_unique <int >(10 ); cout << *(w.get ()) << endl; auto w2 = std::move (w); cout << ((w.get () != nullptr ) ? (*w.get ()) : -1 ) << endl; cout << ((w2.get () != nullptr ) ? (*w2.get ()) : -1 ) << endl; 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 () { { auto wA = shared_ptr <int >(new int (20 )); { auto wA2 = wA; cout << ((wA2.get () != nullptr ) ? (*wA2.get ()) : -1 ) << endl; cout << ((wA.get () != nullptr ) ? (*wA.get ()) : -1 ) << endl; cout << wA2.use_count () << endl; cout << wA.use_count () << endl; } cout << wA.use_count () << endl; cout << ((wA.get () != nullptr ) ? (*wA.get ()) : -1 ) << endl; } auto wAA = std::make_shared <int >(30 ); auto wAA2 = std::move (wAA); cout << ((wAA.get () != nullptr ) ? (*wAA.get ()) : -1 ) << endl; cout << ((wAA2.get () != nullptr ) ? (*wAA2.get ()) : -1 ) << endl; cout << wAA.use_count () << endl; cout << wAA2.use_count () << endl; return 0 ; }
引用计数也会带来一个严重问题:循环引用。即存在一种情况,有两个对象,对象A内部有shared_ptr指针指向B,B中也有shared_ptr指向A,当A使用完毕打算回收内存空间时,会检查内部变量pA,此时会去尝试清理B,但B中也有pB指向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; } }; 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; cout << tB.use_count () << endl; tA->pb = tB; tB->pa = tA; cout << tA.use_count () << endl; cout << tB.use_count () << endl; } 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; cout << tB.use_count () << endl; tA->pb = tB; tB->pa = tA; cout << tA.use_count () << endl; cout << tB.use_count () << endl; } 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中的指针就是引用)
在指针使用上,我们会遇到一些问题:
空指针
野指针(没有初始化)
不知不觉改变了指针的值,我们却仍然在使用
使用引用,我们可以避免这些问题:
不存在空引用;
引用在创建时就必须被初始化;
一个引用永远指向它初始化的那个对象,不允许被修改。
sizeof(引用)得到的是所指向的变量或者对象的大小,sizeof(指针)得到的是指针本身的大小。如果返回动态分配的对象或内存,千万要使用指针,使用引用很可能引起内存泄漏。
引用的基本使用:可以认为是指定变量的别名,使用时可以认为是变量本身:
1 2 3 4 5 6 int x1 = 1 ,x2 = 3 ;int & rx = x1; rx = 2 ; cout << x1 << rx << endl; rx = x2; cout << x1 << x2 << endl;
当我们在函数中需要操作形参并且返回时一并返回,这时候我们就可以传递引用。
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;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 ; }
引用也可以做类的数据成员:引用类型成员的初始化不能在构造函数中执行,必须用到初始化列表;凡是有引用类型的数据成员,必须定义构造函数。
1 2 3 4 5 6 7 8 9 class ConstRef { public : ConstRef (int ii) : i (ii), ci (i), ri (ii) { } private : int i; const int ci; int & ri; };
C++为什么要同时存在指针和引用?在java语言中我们直接使用引用,传统C语言我们都使用指针。C++可以认为是夹在C和java之间的一种。之所以要使用引用是为了支持函数的运算符重载。而C++为了兼容C语言不能摒弃指针。
在函数传递参数的时候,对于内置基础类型(int、double等)而言,在函数中传递值更高效(pass
by value);在面向对象中自定义类型而言,在函数中传递const引用更高效(pass
by reference to const)。