C++易混淆知识点整理
// 1 ///////////////////////////////////////////////////////////////////////
// 常量指针:,指针可修改,变量不可修改(只是对于p指针来说,不能通过p指针来修改其指向的变量的值,但如果有其他指针ptr同时指向此变量,则可通过ptr修改变量的值)
// 也即,变量的不可修改只是对于指针p而言,其他指针可以正常修改变量的值 参见Effective C++ —— 让自己习惯C++(一)条款03 const int *p; int const *p; // 指针常量:指针不可修改(指针的指向不可修改,也即不能将p重指向其他变量),变量可修改(可通过指针p修改变量的值,当然了,也肯定可以通过其他指针修改此变量的值) int *const p; // 指针解引用之前,一定确保已被初始化为一个确定/合适的地址 int *p; // 分配了指针p的内存,但并没有初始化,指针所指向的数据的内存不知道 // 指向数组的指针:指向一个数组的指针 int *p = new int[5]; int a[5] = {1, 2, 3, 4, 5}; int (*p)[5] = &a; // 指针数组:数组的元素是地址(指针) int b1,b2,b3,b4,b5; int *ptr[5] = {&b1, &b2, &b3, &b4, &b5}; // 指向指针数组的指针 int* (*p)[5] = &ptr; // 指针函数: 函数返回类型是某一类型的指针 int *f(x,y); // 函数指针: 指向函数的指针变量,即本质是一个指针变量 int (*f) (int x); /* 声明一个函数指针 */ f=func; /* 将func函数的首地址赋给指针f */ // 函数指针数组:数组的每一个元素都是一个函数指针,符合函数指针数组所规定的返回值和参数标志 double (*f_attr[])(double, double); // 指向函数指针数组的指针 double (*(*f_attr)[])(double, double);
#include <stdio.h> int main(int argc, char *argv[]) { int a = 12; int b = 13; // 常量指针,ptr1指针的指向可以修改 // 但对于指针ptr1,不能通过其修改所指变量a // 可以通过其他方式修改变量a,变量a只对于ptr1是常量 const int* ptr1 = &a; ptr1 = &b; // ok // *ptr1 = b; // Error:assignment of read-only location a = b; // ok // 指针常量,ptr2指针的指向不可修改 // 但指针所指向的变量可以被修改 int* const ptr2 = &a; // ptr2 = &b; //Error:assignment of read-only variable `ptr2' *ptr2 = b; // ok return 0; }
/* ============================================================================ Name : mytestPC.c Author : yangxt Version : Copyright : Your copyright notice Description : Hello World in C, Ansi-style ============================================================================ */ #include <stdio.h> #include <stdlib.h> int nValue = 0; int main(void) { // 栈区向地址减小的方向增长(向下生长) int* ip1, * ip2, ivalue; char* cp1, * cp2, cvalue; ip1 = (int*)0x500; ip2 = (int*)0x518; printf("ip2=%d, ip1=%d, ip2 - ip1 = %d\n", ip2, ip1, ip2 - ip1); printf("sizeof(ip1)=%d\n", sizeof(ip1)); ivalue = ip2 - ip1; // int 类型指针运算以int的长度[4Bytes]为一个单位 cp1 = (char*)0x500; cp2 = (char*)0x518; printf("cp2=%d, cp1=%d, cp2 - cp1 = %d\n", cp2, cp1, cp2 - cp1); printf("sizeof(cp1)=%d\n", sizeof(cp1)); // 无论什么类型的指针,对于指针本身来说,它都是占据4Bytes cvalue = cp2 - cp1; // char 类型指针运算以char的长度[1Byte]为一个单位 printf("%d, %d\n", ivalue, cvalue); //output: // ip2=1304, ip1=1280, ip2 - ip1 = 6 // sizeof(ip1)=4 // cp2=1304, cp1=1280, cp2 - cp1 = 24 // sizeof(cp1)=4 // 6, 24 return EXIT_SUCCESS; }
// 2 ///////////////////////////////////////////////////////////////////////
临时变量/引用参数和const
--对于一个函数:
(1)如果其接收常规引用参数,其意图在于修改变量,此时C++会禁止创建临时变量;(因为如果接受引用参数的函数的意图是修改作为参数传递的变量,则创建临时变量将阻止这种意图的实现)
(2)如果其接收const引用,其意图在于不修改只使用变量,此时在满足下面两个条件之一的情况下,C++便会为变量创建临时变量:
1. 实参的类型正确,但不是左值;
2. 实参的类型不正确,但可以转换为正确的类型.
--应尽可能使用const引用:
1. 使用const可以避免无意中修改数据的编程错误;
2. 使用const使函数能够处理const和非const实参,否则将只能接受非const数据;
3. 使用const引用使函数能够正确生成并使用临时变量;
--引用非常适合用于结构和类,可以避免大量拷贝导致的资源消耗;同时也可以令返回值为引用,需要注意的是要避免返回函数终止时不再存在的内存单元引用.
--对于返回类型,常规(非引用)返回类型为右值,不能通过地址访问;而引用返回类型为左值,可以被修改和取地址;当然,const引用返回不能被修改.
--临时变量和函数的非引用返回值都是右值,不可取地址,不可被赋值.
函数的返回值,如果是内置类型的非引用返回值,那么不可被修改(修改(赋值)右值不合法);但如果是自定义类型的非引用返回值呢?<Effective C++>(3e)条款03中提供了一个可修改的例子,但它同时也表明:一个"良好的用户定义类型"会避免无端地与内置类型不兼容.所以普遍来说:改动函数非引用返回值从来就不合法.
// 3 ///////////////////////////////////////////////////////////////////////
类成员函数的重载、重写、和覆盖区别
a.成员函数被重载的特征:
(1)相同的范围(在同一个类中);
(2)函数名字相同;
(3)参数不同;
(4)virtual关键字可有可无。
b.覆盖是指派生类函数覆盖基类函数,特征是:
(1)不同的范围(分别位于派生类与基类);
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有virtual关键字。
c.“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)
// 4 ///////////////////////////////////////////////////////////////////////
5种变量存储方式
存储描述 持续性 作用域 链接性 如何声明
-------- -------- -------- ------- -----------
自动 自动 代码块 无 在代码块中
寄存器 自动 代码块 无 在代码块中,使用关键字register
静态,无链接性 静态 代码块 无 在代码块中,使用关键字static
静态,外部链接性 静态 文件 外部 不在任何函数内
静态,内部链接性 静态 文件 内部 不在任何函数内,使用关键字static
// 5 ///////////////////////////////////////////////////////////////////////
切记:声明类只是描述了对象的形式,并没有创建对象.因此,在创建对象前,将没有用于存储值的空间.
所以,对于需要在类中声明常量,可以采用枚举,同时,针对于静态常整型变量,也可以在类声明时直接赋值;
注意:在类中声明枚举并不会创建类数据成员.也就是说,所有对象中都不包含枚举.
同时,C++11提供了新枚举--作用域内枚举.常规枚举会自动转换为整型,但是作用域枚举不能隐式地转换为整型.
// 6 ///////////////////////////////////////////////////////////////////////
类的自动转换和强制类型转换
1. 在C++中,只有接受一个参数的构造函数才能作为转换函数(如果有2个参数,第二个参数提供默认值,同样可以用来作为转换函数).可以通过使用explicit关闭这种特性.
2. C++类的转换函数(将类对象转换成基础类型):
(1)转换函数必须是类方法;
(2)转换函数不能指定返回类型;
(3)转换函数不能有参数;
例如,转换为typeName类型的函数原型如下:
[explicit] operator typeName()
注:typeName指出了要转换成的类型,因此不需要指定返回类型.转换函数是类方法意味着:它需要通过类对象来调用,从而告知函数要转换的值.因此,函数不需要参数.
在C++11 中,可以通过explicit将转换运算符声明为显式.
// 7 ///////////////////////////////////////////////////////////////////////
/* * stringImpl.cpp * * Created on: 2019年11月18日 * Author: yangxt */ #include <iostream> #include <string.h> //已知类String的原型如下,请编写String的4个函数: class String { public: String(const char *str = NULL);// 普通构造函数 String(const String &other);// 拷贝构造函数 String(); // 无参构造函数 ~String(void);// 析构函数 String & operator = (const String &other);// 赋值函数 private: // 类含有指针成员,需要进行深拷贝 char *m_data;// 用于保存字符串 }; // 无参构造函数 String::String() { // 无参构造函数,需要置m_data==NULL m_data = NULL; } // String的析构函数 String::~String(void) { delete[] m_data; // 或delete m_data; } //普通构造函数 String::String(const char *str) { if (str == NULL) { m_data = new char[1];// 对空字符串自动申请存放结束标志'\0'的,加分点:对m_data加NULL判断 *m_data = '\0'; } else { int length = strlen(str); m_data = new char[length + 1];// 若能加 NULL 判断则更好 if(m_data == NULL){ exit(1); } strcpy(m_data, str); } } //拷贝构造函数 String::String(const String &other)// 输入参数为const型 { int length = strlen(other.m_data); // 分配内存,进行深拷贝 m_data = new char[length + 1];// 若能加 NULL 判断则更好 if(m_data == NULL){ exit(1); } strcpy(m_data, other.m_data); } //赋值函数 //string类的赋值运算符 参见 Effective C++ —— 构造/析构/赋值运算(二)条款10 String & String::operator = (const String &other) // 输入参数为const型 { // 必须先检查 自赋值 if (this == &other) return *this; //释放原有的内存资源 if (m_data) delete[] m_data; // 分配内存:进行深拷贝 int length = strlen(other.m_data); m_data = new char[length + 1];//对m_data加NULL判断 if(m_data == NULL){ exit(1); } strcpy(m_data, other.m_data); return *this;//返回本对象的引用 }
补充一点【拷贝构造函数和拷贝赋值函数的调用时机分析】
include<iostream> using namespace std; class MyClass { public: MyClass(int i = 0) { cout << i; } MyClass(const MyClass &x) { cout << 2; } MyClass &operator=(const MyClass &x) { cout << 3; return *this; } ~MyClass() { cout << 4; } }; // 题目来源:牛客网 int main() { MyClass obj1(1), obj2(2); //obj3还不存在,所以调用拷贝构造函数输出2[最终输出122444] //如果obj3存在,obj3=obj,则调用复制运算符重载函数,输出3[最终输出1203444] MyClass obj3 = obj1; return 0; }
// 8 ///////////////////////////////////////////////////////////////////////
虚函数的工作原理:
通常,编译器处理虚函数的方法是:给每个对象添加一个隐藏成员.隐藏成员中保存了一个指向函数地址数组的指针.这种数组称为虚函数表(virtual function table,vtbl).虚函数表中存储了为类对象进行声明的虚函数的地址.例如,基类对象包含一个指针,该指针指向基类中所有虚函数的地址表.派生类对象将包含一个指向独立地址表的指针.如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址;如果派生类没有重新定义虚函数,该vtbl将保存函数原始版本的地址.如果派生类定义了新的虚函数,则该函数的地址也将被添加到vtbl中.注意,无论类中包含的虚函数是1个还是10个,都只需要在对象中添加1个地址成员,只是表的大小不同而已.
注意:
1. 通常应给基类提供一个虚析构函数,即使它并不需要析构函数.因为如果你定义virtual虚函数,意味着使用多态,那么你就要为动态对象占有的内存定义一个虚析构函数.否则的话,当你使用一个指向派生类对象的基类指针进行析构操作的时候,你将仅仅只是释放了基类对象相关数据,而派生类对象依然没能被成功释放.
2. 友元不能是析构函数,因为友元不是类成员,而只有类成员才能是虚函数.
3. 当定义了一个纯虚函数的时候,表明这个类为抽象基类.抽象基类一般是作为其他类的基类,并且不能用来创建对象.
// 9 ///////////////////////////////////////////////////////////////////////
继承和动态内存分配(假设基类使用了动态内存分配)
// Base Class Using DMA class baseDMA{ private: char *label; int rating; public: baseDMA(const char *l = "null", int r = 0); baseDMA(const baseDMA &rs); virtual ~baseDMA(); baseDMA & operator=(const baseDMA & rs); .... }
声明中包含了构造函数使用new是需要的特殊方法:析构函数,拷贝构造函数和重载赋值运算符.
1. 派生类不使用new:那么不需要为派生类定义显式析构函数,拷贝构造函数和赋值运算符.
(1)由于派生类不需要执行任何特殊操作,所以默认析构函数是合适的;
(2)默认拷贝构造函数执行成员复制,这对于动态内存分配来说是不合适的.但对于没有使用new的派生类来说却是可以满足要求.在执行类成员或继承的类组件的复制时,派生类会使用基类的拷贝构造函数来复制派生类对象中的基类部分;
(3)对于赋值来说,也是如此.
2. 派生类使用了new:派生类需要显式定义析构函数,拷贝构造函数和赋值运算符.
(1)派生类析构函数自动调用基类的析构函数,故其自身的职责是对派生类构造函数执行的工作进行清理.
(2)拷贝构造函数同理,调用基类相应的拷贝构造函数复制基类部分,派生类部分只能由派生类的拷贝构造函数访问并拷贝.
(3)对于赋值来说,也是如此.
// 10 ///////////////////////////////////////////////////////////////////////
包含和私有继承的比较:
1. 包含提供被显式命名的对象成员;而私有继承提供没有名称的子对象成员.
2. 对于构造函数,包含将使用成员对象赋值语句;继承使用的是成员初始化列表语法,它使用类名而不是成员名来标志构造函数.
3. 使用包含时将使用对象名来调用方法,而使用私有继承时将使用类名和作用域解析运算符来调用方法.
4. 访问基类对象.包含可以直接使用对象名访问;私有继承没有名称,通过强制类型转换,将派生类对象转换为基类对象,进而进行访问.
5. 同第4点,访问基类的友元函数,私有继承可以通过将派生类对象转换成基类对象,匹配基类友元函数参数,进而调用到基类友元函数的目的.
6. 对于程序员来说,包含易于理解,我们可以显式地在包含类中定义被包含类的对像,然后用该对像调用被包含类的成员函数,这会让程序员看上去非常清淅易懂,但私有继承私不这么直观,它使类之间的关系变得更加抽像,并且很多情况下,这种关系比较抽像和复杂,程序员必须处理很多继承带来的问题,比如说派生类与基类的函数重名问题,也就是两义性问题,或者继承一些不该继承的方法或变量,比如说派生类的某个基类上还有基类,那么这个基类的方法也会延续到后面的所有派生类中.显然,作为一个位地继承层次之外的类,包含类则不存在这样的问题.
7. 包含类还可以包含同一个类的多个对像,但是私有继承不能这么做.
8. 私有继承有包含所没有的特殊属性,比如说私有继承的派生类与基类是继承与被继承的关系,我们知道公有继承的情况下基类的保护成员在派生类中也是保护成员,我们可以像在基类中一样访问它,私有继承的情况下基类的保护成员在派生类中是私有成员,我们可以在派生类中设置成员函数来访问它,但是包含与被包含却没有这层关系,由于所包含的对像位于继承层次之外,并且保护成员只对派生类开放,因此包含类不能通过所包含的对像来访问该对像的保护成员.
9. 私有继承的另外一个优势是使派生类可以重新定义基类的虚函数,这个被重新定义的虚函数只能在类中使用,包含则没有这个特性.
参见 Effective C++ —— 继承与面向对象设计(六) 条款39
// 11 ///////////////////////////////////////////////////////////////////////
类型转换运算符
(1)dynamic_cast:在类的继承体系中向上转换(派生类->基类),而不允许其他转换(空指针);
(2)const_cast:删除const限定符;
(3)static_cast:与通用的类型转换运算符相同;
(4)reinterpret_cast:用于危险的类型转换,也可以把指针类型转换为足以存储指针表示的整型.
// 12 ///////////////////////////////////////////////////////////////////////
Effective C++ —— 实现(五) 条款30详细介绍了inline函数
// 13 ///////////////////////////////////////////////////////////////////////
#include <stdio.h> int main(int argc, char *argv[]) { int i = 10; int j = 10; i++ = 5; // 非左值,不能赋值 ++j = 6; // 返回引用,左值,可以赋值 printf("%d, %d\n", i, j); return 0; } // STL 容器 deque 关于"++"运算符的实现如下: // 前置式,如 "++i",返回引用, 左值,可以赋值 T& operator++() { ++cur; if (cur == last) { // to do } return *this; } // 后置式,如"i++",带参,产生临时对象,性能上不如"++i" // 非左值,不能赋值 T operator++(int) { T tmp = *this; ++*this; return tmp; }
// 14 ///////////////////////////////////////////////////////////////////////
BOOL,int,float,指针变量 与“零值”比较的if语句
以下摘自《林锐-高质量c++c编程指南》
/* ---------------------------------------------------------------------------- */
if语句是C++/C语言中最简单、最常用的语句,然而很多程序员用隐含错误的方式写if语句。本节以“与零值比较”为例,展开讨论。
(一). 布尔变量与零值比较
【规则4-3-1】不可将布尔变量直接与TRUE、FALSE或者1、0进行比较。
根据布尔类型的语义,零值为“假”(记为FALSE),任何非零值都是“真”(记为TRUE)。TRUE的值究竟是什么并没有统一的标准。例如Visual C++将TRUE定义为1,而Visual Basic则将TRUE定义为-1。假设布尔变量名字为flag,它与零值比较的标准if语句如下:
if (flag) // 表示flag为真 if (!flag) // 表示flag为假
其它的用法都属于不良风格,例如:
if (flag == TRUE) if (flag == 1 ) if (flag == FALSE) if (flag == 0)
(二). 整型变量与零值比较
【规则4-3-2】应当将整型变量用“==”或“!=”直接与0比较。
假设整型变量的名字为value,它与零值比较的标准if语句如下:
if (value == 0) if (value != 0)
不可模仿布尔变量的风格而写成
if (value) // 会让人误解 value是布尔变量 if (!value)
(三). 浮点变量与零值比较
【规则4-3-3】不可将浮点变量用“==”或“!=”与任何数字比较。
千万要留意,无论是float还是double类型的变量,都有精度限制。所以一定要避免将浮点变量用“==”或“!=”与数字比较,应该设法转化成“> =”或“ <=”形式。
假设浮点变量的名字为x,应当将
if (x == 0.0) // 隐含错误的比较
转化为
if ((x> =-EPSINON) && (x <=EPSINON))
其中EPSINON是允许的误差(即精度)。
(四). 指针变量与零值比较
【规则4-3-4】应当将指针变量用“==”或“!=”与NULL比较。
指针变量的零值是“空”(记为NULL)。尽管NULL的值与0相同,但是两者意义不同。假设指针变量的名字为p,它与零值比较的标准if语句如下:
if (p == NULL) // p与NULL显式比较,强调p是指针变量 if (p != NULL)
不要写成
if (p == 0) // 容易让人误解p是整型变量 if (p != 0)
或者
if (p) // 容易让人误解p是布尔变量 if (!p)
(五). 对if语句的补充说明
有时候我们可能会看到 if(NULL==p) 这样古怪的格式。不是程序写错了,是程序员为了防止将 if(NULL==p) 误写成 if(p=NULL),而有意把p和NULL颠倒。编译器认为 if(p=NULL)NULL) 是合法的,但是会指出 if(NULL=p)是错误的,因为NULL不能被赋值。
程序中有时会遇到if/else/return的组合,应该将如下不良风格的程序
if(condition) return x; return y;
改写为
if(condition) { return x; } else { return y; }
或者改写成更加简练的
return (condition ? x : y);
/*************************************************************/
分别给出BOOL,int,float,指针变量 与“零值”比较的 if 语句(假设变量名为var)
解答:
if(!var); //BOOL型变量 if(var==0); // int型变量 const float EPSINON = 0.00001; if((x>=-EPSINON)&&(x<=EPSINON)) // float型变量 if(var==NULL); // 指针变量
考查对0值判断的“内功”,BOOL型变量的0判断完全可以写成if(var==0),而int型变量也可以写成if(!var),指针变量的判断也可以 写成if(!var),上述写法虽然程序都能正确运行,但是未能清晰地表达程序的意思。一般的,如果想让if判断一个变量的“真”、“假”,应直接使用 if(var)、if(!var),表明其为“逻辑”判断;如果用if判断一个数值型变量(short、int、long等),应该用 if(var==0),表明是与0进行“数值”上的比较;而判断指针则适宜用if(var==NULL),这是一种很好的编程习惯。
浮点型变量并不精确,所以不可将float变量用“==”或“!=”与数字比较,应该设法转化成“>=”或“<=”形式。如果写成if (x == 0.0),则判为错,得0分。
以下给出详细原因:浮点数在内存中的存贮机制和整型数不同,有舍入误差,在计算机中用以近似表示任意某个实数。具体的说,这个实数由一个整数或定点 数(即尾数)乘以某个基数(计算机中通常是2)的整数次幂得到,这种表示方法类似于基数为10的科学记数法。 所以浮点数在运算过成功运算通常伴随着因为 无法精确表示而进行的近似或舍入。但是这种设计的好处是可以在固定的长度上存储更大范围的数。
例如,一个指数范围为±4的4位十进制浮点数可以用来表示43210,4.321或0.0004321,但是没有足够的精度来表示432.123和43212.3(必须近似为432.1和43210)。当然,实际使用的位数通常远大于4。
所以浮点数不能够判断相等,像 if(x==0)的这样的编码是不总是正确的,我们在判断浮点数相等时,推荐用范围来确定,若x在某一范围内,我们就认为相等,至于范围怎么定义,要看实际情况而已了,float,和double 各有不同。所以 const float EPSINON = 0.00001; if ((x >= - EPSINON) && (x & lt;= EPSINON) 这样判断是可取的至于为什么取0.00001,可以自己按实际情况定义。
// 15 ///////////////////////////////////////////////////////////////////////
一个由C/C++编译程序占用内存分为以下几个部分
1、栈区(stack)——由编译器自动分配释放 ,存放函数参数值,局部变量值等。其操作方式类似于数据结构中栈。
2、堆区(heap) ——般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中堆是两回事,分配方式倒是类似于链表。
3、全局区(静态区)(static)——,全局变量和静态变量存储是放在一块,初始化全局变量和静态变量在一块区域, 未初始化全局变量和未初始化静态变量在相邻另一块区域。 - 程序结束后由系统释放。-->分别是data区,bbs区
4、文字常量区 ——常量字符串就是放在这里。 程序结束后由系统释放-->coment区
5、程序代码区——存放函数体二进制代码。-->code区
例子:
#include <string> int a=0; //全局初始化区 char *p1; //全局未初始化区 void main() { int b;//栈 char s[]="abc"; //栈 char *p2; //栈 char *p3="123456"; //123456\0在常量区,p3在栈上。 static int c=0; //全局(静态)初始化区 p1 = (char*)malloc(10); p2 = (char*)malloc(20); //分配得来的10和20字节的区域就在堆区。 strcpy(p1,"123456"); //123456\0放在常量区,编译器可能会将它与p3所向"123456\0"优化成一个地方。 }
// 16 ///////////////////////////////////////////////////////////////////////
Union的几点注意事项:
我们知道,联合里面的东西共享内存,所以静态、引用都不能用,因为他们不可能共享内存。
2、类可以放入联合吗?我们先看一个例子:
class Test { public: Test():data(0) { } private: int data; }; typedef union _test { Test test; }UI;
编译通不过,为什么呢?
因为联合里不允许存放带有构造函数、析够函数、复制拷贝操作符等的类,因为他们共享内存,编译器无法保证这些对象不被破坏,也无法保证离开时调用析够函数。
3、又是匿名惹的祸?? 我们先看下一段代码:
class test { public: test(const char* p); test(int in); const operator char*() const {return data.ch;} operator long() const {return data.l;} private: enum type {Int, String }; union { const char* ch; int i; }datatype; type stype; test(test&); test& operator=(const test&); }; test::test(const char *p):stype(String),datatype.ch(p){} test::test(int in):stype(Int),datatype.l(i){}
代码段编译通不过, 让我们来看看构造test对象时发生了什么,当创建test对象时,自然要调用其相应的构造函数,在构造函数中当然要调用其成员的构造函数,所以其要去调用datatype成员的构造函数,但是他没有构造函数可调用,所以出错。
注意了,这里可并不是匿名联合!因为它后面紧跟了个data!
4、如何有效的防止访问出错?
使用联合可以节省内存空间,但是也有一定的风险:通过一个不适当的数据成员获取当前对象的值!为了防止这样的错误,我们必须定义一个额外的对象,来跟踪当前被存储在联合中的值得类型,我们称这个额外的对象为:union的判别式。一个比较好的经验是,在处理作为类成员的union对象时,为所有union数据类型提供一组访问函数。
// 17 ///////////////////////////////////////////////////////////////////////
C/C++中引用变量的几个要点:
1. 什么是“引用”?申明和使用“引用”要注意哪些问题?
答:引用就是某个目标变量的“别名”(alias),对应用的操作与对变量直接操作效果完全相同。申明一个引用的时候,切记要对其进行初始化。引用声明完毕后,相当于目标变量名有两个名称,即该目标原名称和引用名,不能再把该引用名作为其他变量名的别名。声明一个引用,不是新定义了一个变量,它只表示该引用名是目标变量名的一个别名,它本身不是一种数据类型,因此引用本身不占存储单元,系统也不给引用分配存储单元。不能建立数组的引用。
2. 将“引用”作为函数参数有哪些特点?
(1)传递引用给函数与传递指针的效果是一样的。这时,被调函数的形参就成为原来主调函数中的实参变量或对象的一个别名来使用,所以在被调函数中对形参变量的操作就是对其相应的目标对象(在主调函数中)的操作。
(2)使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作;而使用一般变量传递函数的参数,当发生函数调用时,需要给形参分配存储单元,形参变量是实参变量的副本;如果传递的是对象,还将调用拷贝构造函数。因此,当参数传递的数据较大时,用引用比用一般变量传递参数的效率和所占空间都好。
(3)使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调函数中同样要给形参分配存储单元,且需要重复使用"*指针变量名"的形式进行运算,这很容易产生错误且程序的阅读性较差;另一方面,在主调函数的调用点处,必须用变量的地址作为实参。而引用更容易使用,更清晰。
3. 在什么时候需要使用“常引用”?
如果既要利用引用提高程序的效率,又要保护传递给函数的数据不在函数中被改变,就应使用常引用。常引用声明方式:const 类型标识符 &引用名=目标变量名;
例1
int a ;
const int &ra=a;
ra=1; //错误
a=1; //正确
例2
string foo( );
void bar(string & s);
那么下面的表达式将是非法的:
bar(foo( ));
bar("hello world");
原因在于foo( )和"hello world"串都会产生一个临时对象,而在C++中,这些临时对象都是const类型的。因此上面的表达式就是试图将一个const类型的对象转换为非const类型,这是非法的。
引用型参数应该在能被定义为const的情况下,尽量定义为const 。
4. 将“引用”作为函数返回值类型的格式、好处和需要遵守的规则?
格式:类型标识符 &函数名(形参列表及类型说明){ //函数体 }
好处:在内存中不产生被返回值的副本;(注意:正是因为这点原因,所以返回一个局部变量的引用是不可取的。因为随着该局部变量生存期的结束,相应的引用也会失效,产生runtime error!
注意事项:
(1)不能返回局部变量的引用。这条可以参照Effective C++[1]的Item 31。主要原因是局部变量会在函数返回后被销毁,因此被返回的引用就成为了"无所指"的引用,程序会进入未知状态。
(2)不能返回函数内部new分配的内存的引用。这条可以参照Effective C++[1]的Item 31。虽然不存在局部变量的被动销毁问题,可对于这种情况(返回函数内部new分配内存的引用),又面临其它尴尬局面。例如,被函数返回的引用只是作为一个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就无法释放,造成memory leak[内存泄露]。
(3)可以返回类成员的引用,但最好是const。这条原则可以参照Effective C++[1]的Item 30。主要原因是当对象的属性是与某种业务规则(business rule)相关联的时候,其赋值常常与某些其它属性或者对象的状态有关,因此有必要将赋值操作封装在一个业务规则当中。如果其它对象可以获得该属性的非常量引用(或指针),那么对该属性的单纯赋值就会破坏业务规则的完整性。
(4)流操作符重载返回值申明为“引用”的作用:
流操作符<<和>>,这两个操作符常常希望被连续使用,例如:cout << "hello" << endl; 因此这两个操作符的返回值应该是一个仍然支持这两个操作符的流引用。可选的其它方案包括:返回一个流对象和返回一个流对象指针。但是对于返回一个流对象,程序必须重新(拷贝)构造一个新的流对象,也就是说,连续的两个<<操作符实际上是针对不同对象的!这无法让人接受。对于返回一个流指针则不能连续使用<<操作符。因此,返回一个流对象引用是惟一选择。这个唯一选择很关键,它说明了引用的重要性以及无可替代性,也许这就是C++语言中引入引用这个概念的原因吧。 赋值操作符=。这个操作符象流操作符一样,是可以连续使用的,例如:x = j = 10;或者(x=10)=100;赋值操作符的返回值必须是一个左值,以便可以被继续赋值。因此引用成了这个操作符的惟一返回值选择。
例3
#i nclude <iostream.h>
int &put(int n);
int vals[10];
int error=-1;
void main()
{
put(0)=10; //以put(0)函数值作为左值,等价于vals[0]=10;
put(9)=20; //以put(9)函数值作为左值,等价于vals[9]=20;
cout<<vals[0];
cout<<vals[9];
}
int &put(int n)
{
if (n>=0 && n<=9 ) return vals[n];
else { cout<<"subscript error"; return error; }
}
(5)在另外的一些操作符中,却千万不能返回引用:+-*/ 四则运算符。它们不能返回引用,Effective C++[1]的Item23详细的讨论了这个问题。主要原因是这四个操作符没有side effect,因此,它们必须构造一个对象作为返回值,可选的方案包括:返回一个对象、返回一个局部变量的引用,返回一个new分配的对象的引用、返回一个静态对象引用。根据前面提到的引用作为返回值的三个规则,第2、3两个方案都被否决了。静态对象的引用又因为((a+b) == (c+d))会永远为true而导致错误。所以可选的只剩下返回一个对象了。
5. “引用”与多态的关系?
引用是除指针外另一个可以产生多态效果的手段。这意味着,一个基类的引用可以指向它的派生类实例。
例4
Class A; Class B : Class A{...}; B b; A& ref = b;
6. “引用”与指针的区别是什么?
指针通过某个指针变量指向一个对象后,对它所指向的变量间接操作。程序中使用指针,程序的可读性差;而引用本身就是目标变量的别名,对引用的操作就是对目标变量的操作。此外,就是上面提到的对函数传ref和pointer的区别。
7. 什么时候需要“引用”?
流操作符<<和>>、赋值操作符=的返回值、拷贝构造函数的参数、赋值操作符=的参数、其它情况都推荐使用引用。
// 18 ///////////////////////////////////////////////////////////////////////
#include<iostream> // (1)对象不包含虚函数表,只有虚指针,类才包含虚函数表,派生类会生成一个兼容基类的虚函数表; // (2)virtual table 虚函数表 存放在 全局变量区; // (3)子类多重继承自多个均带有虚函数的基类,那么子类会生成多个分别兼容各个基类的虚函数表,并且子类对象保有多个虚指针。 // (4)虚指针存在于对象实例地址的最前面,保证虚函数表有最高的性能,这个虚指针指向虚函数表; // (5)任何妄图使用父类指针想调用子类中的未覆盖父类的成员函数的行为都会被编译器视为非法; // (6)虚析构函数使得在删除指向子类对象的基类指针时可以调用子类的析构函数达到释放子类中堆内存的目的,而防止内存泄露的; class A //class's size is 1 { }; class B //4字节对齐,class's size is 8 { private: int a; char aa; short bb; }; class B1 //class's size is 1 { private: void fun(){} }; class C //class's size is 4 { virtual void fun() = 0; virtual ~C(){}; }; class D //class's size is 4 { public: // 下面3个虚函数 fun/fun1/~D 在虚函数表中将按其在类中声明的顺序排列,虚析构函数也不例外 virtual void fun(){ std::cout << "this is a D-fun()." << std::endl;} virtual void fun1(){ std::cout << "this is a D-fun1()." << std::endl; } virtual ~D(){ std::cout << "this is a ~D()." << std::endl; }; }; class D0 : public D //class's size is 4 { public: void fun(){ std::cout << "this is a D0-fun()." << std::endl; } void fun1(){ std::cout << "this is a D0-fun1()." << std::endl; } ~D0(){ std::cout << "this is a ~D0()." << std::endl; }; }; class D1 //class's size is 4 { virtual void fun(){ std::cout << "this is a D2."; } virtual void fun1() { std::cout << "this is a D2."; } }; class D2 : public D1, public D //class's size is 8 { virtual void fun(){ std::cout << "this is a D3."; } virtual void fun1() { std::cout << "this is a D3."; } }; class D3 : public D1, public D //class's size is 8 { virtual void fun3() { std::cout << "this is a D3."; } }; class D4 : public D1, public B1 //class's size is 4 { virtual void fun3(){ std::cout << "this is a D3."; } }; class E //class's size is 1 { void show() { std::cout << "THIS IS E"; } }; class F : public A //class's size is 4 { virtual void show(){ std::cout << "THIS IS E"; } }; int main() { typedef void(*Fun)(void); Fun pFun = NULL; D* testD = new D0(); // 虚指针存在于对象实例地址的最前面,保证虚函数表有最高的性能 std::cout << "虚指针vptr地址:" << (int*)testD << std::endl; std::cout << "vptr指向的虚函数表地址(对vptr解引用):" << (int*)*(int*)testD << std::endl; std::cout << "虚函数表 — 第一个函数地址:" << (int*)(*(int*)testD) << std::endl; std::cout << "虚函数表 — 第二个函数地址:" << (((int*)(*(int*)testD))+1) << std::endl; std::cout << "虚函数表 — 第三个函数地址:" << (((int*)(*(int*)testD))+2) << std::endl; // 多态:子类中函数覆盖了父类虚函数,在虚函数表中:覆盖的函数被放到了虚表中原来父类虚函数的位置 testD->fun(); pFun = (Fun)*(int*)(*(int*)testD); pFun(); // 虚函数fun1 在基类D中第2个被声明,故而在虚函数表的第2元素 testD->fun1(); pFun = (Fun)*(((int*)(*(int*)testD))+1); pFun(); // "delete testD;" 既调用了子类的析构函数,又调用了父类的析构函数 -> 原理解析: // (1)父类的构造函数/析构函数与子类的构造函数/析构函数会形成多态,但是当父类的构造函数/析构函数即使被声明virtual, // 子类的构造/析构方法仍无法覆盖父类的构造方法和析构方法。这是由于父类的构造函数和析构函数是子类无法继承的,也就是说每一个类都有自己独有的构造函数和析构函数。 // (2)当delete父类的指针时,由于子类的析构函数与父类的析构函数构成多态,所以会先调用子类的析构函数; // (3)而delete运算符的机制又决定了: delete 父类指针所指的空间,会调用父类的析构函数。 delete testD; // 虚指针vptr地址:0x2fb18a0 // vptr指向的虚函数表地址(对vptr解引用):0x405480 // 虚函数表 — 第一个函数地址:0x405480 // 虚函数表 — 第二个函数地址:0x405484 // 虚函数表 — 第三个函数地址:0x405488 // this is a D0-fun(). // this is a D0-fun(). // this is a D0-fun1(). // this is a D0-fun1(). // this is a ~D0(). // this is a ~D(). std::cout << "sizeof(A) = " << sizeof(A) << std::endl; std::cout << "sizeof(B) = " << sizeof(B) << std::endl; std::cout << "sizeof(B1) = " << sizeof(B1) << std::endl; std::cout << "sizeof(C) = " << sizeof(C) << std::endl; std::cout << "sizeof(D) = " << sizeof(D) << std::endl; std::cout << "sizeof(D0) = " << sizeof(D0) << std::endl; std::cout << "sizeof(D1) = " << sizeof(D1) << std::endl; std::cout << "sizeof(D2) = " << sizeof(D2) << std::endl; std::cout << "sizeof(D3) = " << sizeof(D3) << std::endl; std::cout << "sizeof(D4) = " << sizeof(D4) << std::endl; std::cout << "sizeof(E) = " << sizeof(E) << std::endl; std::cout << "sizeof(F) = " << sizeof(F) << std::endl; getchar(); // sizeof(A) = 1 // sizeof(B) = 8 // sizeof(B1) = 1 // sizeof(C) = 4 // sizeof(D) = 4 // sizeof(D0) = 4 // sizeof(D1) = 4 // sizeof(D2) = 8 // sizeof(D3) = 8 // sizeof(D4) = 4 // sizeof(E) = 1 // sizeof(F) = 4 }
// 19 ///////////////////////////////////////////////////////////////////////
// 深拷贝和浅拷贝 要点整理: //(1)浅拷贝(位拷贝)只是对指针的拷贝,拷贝后两个指针指向同一个内存空间(默认拷贝构造函数),两个对象析构的时候会导致对同一个内存空间进行两次内存释放操作; //(2)深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针(自定义拷贝构造函数)。 //总结:在对含有指针成员的对象进行拷贝时,必须要自己定义拷贝构造函数,使拷贝后的对象指针成员有自己的内存空间,即进行深拷贝,这样就避免了内存泄漏发生。 //注意: //1、当函数的参数为对象时,实参传递给形参的实际上是实参的一个拷贝对象,系统自动通过拷贝构造函数实现; //2、当函数的返回值为一个对象时,该对象实际上是函数内对象的一个拷贝,用于返回函数调用处。 //扩展: //1、浅拷贝带来问题的本质在于析构函数释放多次堆内存,使用std::shared_ptr,可以完美解决这个问题; //2、在string类的实现中,就包含了在拷贝构造函数中对于深拷贝操作的处理; #include <iostream> #include <string.h> using namespace std; class Student { private: int num; //类中有指针成员,需要注意对象拷贝时的深拷贝 char *name; public: Student(); ~Student(); Student(const Student &s);//拷贝构造函数,const防止对象被改变 }; Student::Student() { name = new char(20); cout << "Student" << endl; } Student::~Student() { cout << "~Student " << (int)name << endl; delete name; name = NULL; } // 浅拷贝:没有自定义拷贝构造函数的情况下,会调用默认拷贝构造函数进行浅拷贝(只会复制类指针成员的值,不会复制指针所指向的内容) // 深拷贝:自定义拷贝构造函数实现深拷贝(复制类指针成员所指向的内容); // test:可以尝试把自定义拷贝构造函数屏蔽,看看程序执行结果(验证释放同一内存空间两次会导致什么结果) Student::Student(const Student &s) { name = new char(20); memcpy(name, s.name, strlen(s.name)); cout << "copy Student" << endl; } int main() { {// 花括号让s1和s2变成局部对象,方便测试 Student s1; // 复制对象,如果有自定义拷贝构造函数则调用自定义拷贝构造函数,否则调用默认拷贝构造函数 Student s2(s1); } system("pause"); return 0; } //output: //Student //copy Student //~Student 1120432 //~Student 1120416
// 20 ///////////////////////////////////////////////////////////////////////
智能指针的常见用法和陷阱:
#include <memory> #include <iostream> #include <string> #include <vector> // C++98 & C++11 class Simple { public: Simple(int param = 0) { number = param; std::cout << "Simple: " << number << std::endl; } ~Simple() { std::cout << "~Simple: " << number << std::endl; } void PrintSomething() { std::cout << "PrintSomething: " << info_extend.c_str() << std::endl; } std::string info_extend; int number; }; void TestAutoPtrC98() { // C++11 不建议再使用std::auto_ptr -> 'template<class> class std::auto_ptr' is deprecated [-Wdeprecated-declarations] std::auto_ptr<Simple> my_memory(new Simple(1)); // 创建对象,输出:Simple:1 if (my_memory.get()) { // 判断智能指针是否为空 my_memory->PrintSomething(); // 使用 operator-> 调用智能指针对象中的函数 // my_memory.get()->info_extend = "Addition"; // 使用 get() 返回裸指针,然后给内部对象赋值 my_memory->info_extend = "Addition"; // 和“my_memory.get()->info_extend = "Addition";”等价 my_memory->PrintSomething(); // 再次打印,表明上述赋值成功 (*my_memory).info_extend += " other"; // 使用 operator* 返回智能指针内部对象,然后用“.”调用智能指针对象中的函数 my_memory->PrintSomething(); // 再次打印,表明上述赋值成功 } // output: // Simple: 1 // PrintSomething: // PrintSomething: Addition // PrintSomething: Addition other // ~Simple: 1 } // C++11 提供了 std::shared_ptr(共享) 和 std::unique_ptr(独占),并不建议再使用std::auto_ptr智能指针 int TestSharedPtrC11(void) { // two shared pointers representing two persons by their name std::shared_ptr<std::string> pNico(new std::string("nico")); std::shared_ptr<std::string> pJutta(new std::string("jutta"), // deleter (a lambda function) [](std::string *p) { std::cout << "delete " << *p << std::endl; delete p; } ); // capitalize person names (*pNico)[0] = 'N'; pJutta->replace(0, 1, "J"); // put them multiple times in a container std::vector<std::shared_ptr<std::string>> whoMadeCoffee; whoMadeCoffee.push_back(pJutta); whoMadeCoffee.push_back(pJutta); whoMadeCoffee.push_back(pNico); whoMadeCoffee.push_back(pJutta); whoMadeCoffee.push_back(pNico); // print all elements for (auto ptr : whoMadeCoffee) std::cout << *ptr << " "; std::cout << std::endl; // overwrite a name again *pNico = "Nicolai"; // print all elements for (auto ptr : whoMadeCoffee) std::cout << *ptr << " "; std::cout << std::endl; // print some internal data std::cout << "use_count: " << whoMadeCoffee[0].use_count() << std::endl; // 默认内存释放器并不能释放数组内存空间,而是要我们自己提供内存释放器 std::shared_ptr<int> pYangxt(new int[10], // deleter (a lambda function) [](int *p) { std::cout << "pYangxt=0 -> delete[] p " << std::endl; delete[] p; } ); // 后定义的指针,函数退出的时候会先被释放(局部变量存放在栈中,先进后出) std::shared_ptr<int> pYangxt1(new int(), // deleter (a lambda function) [](int *p) { std::cout << "pYangxt=1 -> delete p " << std::endl; delete p; } ); return 0; //output: // Jutta Jutta Nico Jutta Nico // Jutta Jutta Nicolai Jutta Nicolai // use_count: 4 // pYangxt=1 -> delete p // pYangxt=0 -> delete[] p // delete Jutta // 关于程序的几点说明: // 1)对智能指针pNico的拷贝是浅拷贝,所以当我们改变对象“Nico”的值为“Nicolai”时,指向它的指针都会指向新值; // 2)指向对象“Jutta”的有四个指针:pJutta和pJutta的三份被安插到容器内的拷贝,所以上述程序输出的use_count为4; // 3)shared_ptr本身提供默认内存释放器(default deleter),调用的是delete,不过只对“由new建立起来的单一对象”起作用。当然我们也可以自己定义内存释放器,就如上述程序 }
智能指针常见用法
class Task { int mId; Task(int id ) : mId(id) { std::cout << "Task::Constructor" << std::endl; } ~Task() { std::cout << "Task::Destructor" << std::endl; } }; // 使用 std::make_unique 创建 unique_ptr 对象 / C++14 std::unique_ptr<Task> taskPtr1 = std::make_unique<Task>(11); // 通过原始指针创建 unique_ptr 实例 std::unique_ptr<Task> taskPtr2(new Task(22)); //通过 unique_ptr 访问其成员 std::cout << taskPtr1->mId << std::endl; // 获取管理对象的原始指针,并不交出所有权 Task *p1 = taskPtr1.get(); // 执行完该语句,taskPtr1 不为 nullptr // 返回关联指针,并释放关联指针的所有权,taskPtr3 现在为空 std::unique_ptr<Task> taskPtr3(new Task(33)); Task *p3 = taskPtr3.release(); // 执行完该语句,taskPtr3 为 nullptr // std::unique_ptr<Task> taskPtr4 = new Task(44); // 编译错误 : 不能通过赋值的方法创建对象 // std::unique_ptr<Task> taskPtr5 = taskPtr2; // 编译错误 : unique_ptr 不能复制 // 但可以把 taskPtr2 中关联指针的所有权转移给 taskPtr5 std::unique_ptr<Task> taskPtr5 = std::move(taskPtr2); // 执行完该语句,taskPtr2 为 nullptr // 不要与裸指针混用,也尽量不要让unique_ptr指针去维护已经使用new返回值初始化的指针 int* ptr = new int(10); unique_ptr<int> ptr1(ptr); // 可以,但最好不要这样 unique_ptr<int> ptr2(new int(11)); // 这样最好,在其作用域内,有且仅有一个指针去维护这片堆区内存空间,避免发生内存重复释放 // 向函数传入unique_ptr参数时,必须要使用“引用传参,值传递会将指针指向的地址拷贝至一个“作用域仅在函数之内的临时指针变量”,这会让 unique_ptr 丢失所有权 void func1(unique_ptr<int>& ptrObj); // 引用传参 // 如果需要转移所有权,也可以使用移动语义 传入或返回 unique_ptr 数据 std::unique<int> func2(std::unique_ptr<int>&& ptr) { // do something with ptr... std::unique_ptr<int> ptr1 = std::make_unique<int>(*ptr); return(std::move(ptr1)); } //智能指针make_unique初始化数组 std::unique_ptr<int[]> ptr1(new int[5]{1,2,3,4,5} }; std::unique_ptr<int[]> ptr2 = std::make_unique<int[]>(10); // 10 表示数组大小
// 21 ///////////////////////////////////////////////////////////////////////
#include <iostream> using namespace std; // 简单实现 基于引用计数的智能指针(shared_ptr),一般都定义成模板类,以支持各种类型指针的管理 // 所有的智能指针都会重载 -> 和 * 操作符,本示例中我们并没有重载,只是简单实现 拷贝/赋值 功能。 // 智能指针还有许多其他功能,比较有用的是自动销毁。这主要是利用栈对象的有限作用域以及临时对象(有限作用域实现)析构函数释放内存。 template<class T> class SmartPtr { public: SmartPtr(T *p); ~SmartPtr(); // 浅拷贝:shared_ptr(共享对象)只有一个对象,其他指针都指向该对象 SmartPtr(const SmartPtr<T> &orig); SmartPtr<T>& operator=(const SmartPtr<T> &rhs); // 浅拷贝 int get_use_count(); private: T *ptr; // 将use_count声明成指针是为了方便对其进行递增或递减操作 int *use_count; }; template<class T> int SmartPtr<T>::get_use_count() { return *use_count; } // 采用初始化列表的方式初始化智能指针的ptr成员 template<class T> SmartPtr<T>::SmartPtr(T *p) : ptr(p) { try { use_count = new int(1); } catch (...) { // 捕获new运算符 分配内存空间失败的异常 delete ptr; ptr = nullptr; use_count = nullptr; cout << "Allocate memory for use_count fails." << endl; exit(1); } cout << "Constructor is called!" << endl; } template<class T> SmartPtr<T>::~SmartPtr() { // 只在最后一个对象引用ptr时才释放内存 if (--(*use_count) == 0) { delete ptr; delete use_count; ptr = nullptr; use_count = nullptr; cout << "Destructor is called!" << endl; } } // 浅拷贝:shared_ptr(共享对象)只有一个对象,其他指针都指向该对象 template<class T> SmartPtr<T>::SmartPtr(const SmartPtr<T> &orig) { ptr = orig.ptr; use_count = orig.use_count; ++(*use_count); cout << "Copy constructor is called!" << endl; } // 重载等号函数不同于复制构造函数,即等号左边的对象可能已经指向某块内存。 // 这样,我们就得先判断左边对象指向的内存已经被引用的次数。如果次数为1, // 表明我们可以释放这块内存;反之则不释放,由其他对象来释放。 template<class T> SmartPtr<T>& SmartPtr<T>::operator=(const SmartPtr<T> &rhs) { // 《C++ primer》:“这个赋值操作符在减少左操作数的使用计数之前使rhs的使用计数加1, // 从而防止自身赋值”而导致的提早释放内存 ++(*rhs.use_count); // 将左操作数对象的使用计数减1,若该对象的使用计数减至0,则删除该对象 if (--(*use_count) == 0) { delete ptr; delete use_count; cout << "Left side object is deleted!" << endl; } ptr = rhs.ptr; use_count = rhs.use_count; cout << "Assignment operator overloaded is called!" << endl; return *this; } // 实现简单智能指针 int TestMySmartPtr() { // Test Constructor and Assignment Operator Overloaded SmartPtr<int> p1(new int(0)); cout << "p1.use_count = " << p1.get_use_count() << endl; // Test Copy Constructor SmartPtr<int> p2(p1); cout << "p1.use_count = " << p1.get_use_count() << endl; cout << "p2.use_count = " << p2.get_use_count() << endl; // Test Assignment Operator Overloaded SmartPtr<int> p3(new int(1)); p3 = p1; cout << "p1.use_count = " << p1.get_use_count() << endl; cout << "p2.use_count = " << p2.get_use_count() << endl; cout << "p3.use_count = " << p3.get_use_count() << endl; return 0; // output: // !!!Hello world!!! // Constructor is called! // p1.use_count = 1 // Copy constructor is called! // p1.use_count = 2 // p2.use_count = 2 // Constructor is called! // Left side object is deleted! // Assignment operator overloaded is called! // p1.use_count = 3 // p2.use_count = 3 // p3.use_count = 3 // Destructor is called! }