指针是C语言中的精髓,智能指针是C++中的王炸!
温故知新,可以为师。在开启智能指针学习篇章前,先来探望老朋友—C语言中的精髓—指针。指针的本质是变量,何为变量,变量就是用特定的数据类型做的mooncake,可大可小,例如有 char、int 、short、double等基础类型,还有就是变量在程序运行时可以被改变。
照本宣科,拿课本里的一句话,“指针变量用于保存变量的地址,通过所保存的地址操作变量。” 这看似简单的一句话,被难倒的初学者不计其数。所以有必要用标新立异的形象比喻来阐明其本质,指针和所指向的变量之间的关系类似于邮差与信箱,邮差知道了信箱的具体位置,就可以往信箱塞信或者拿信。
#include<stdio.h>
int main(int argc,char* argv[])
{
int a = 1;
int b = 0;
int* p = &a; // 取地址操作,指针 p 指向了变量 a
*p = 2; // (寄信)相当于 a = 2;运算过后 a 的值为 2;
b = *p + 3; // (取信)等价于 b = a + 3; 即 a 加上3再赋值给 b
// 运算过后 b = 5;
return 0;
}
信随意拿,也随意放,有没有觉得这个邮差权力好像有点大啊 !其实,我想说指针的能力真的是太强大了,但是,有无相生,难易相成,任何事物都有两面性,指针也不例外,如果滥用指针随意修改变量值,可能会导致系统崩溃!!!
为了解决一系列的问题,于是乎,C语言就生产了指针常量,常量指针,常量指针常量,初学者大概率搞不明白说的是什么。话不多说,先上代码!
int b = 1;
const int* p1 = &b;
int* const p2 = &b;
int const* p3 = &b;
const int* const p4 = &b;
指针p1、p2、p3、p4都指向了同一个变量,坦白说,p1指向的内容不可变,p2指向的地址不可变,p3指向的内容不可变,p4指向的内容和地址都不可变。记住一句话就可以了,左数据右地址。
有时候指针很复杂,例如:
int*(*(*p)(int*))(int*);
int (*(*p)(int*))[10];
乍一看还以为是乱码,这样的代码除了吓人之外,没半毛钱用。使用指针的时候,尽量避开形式过于复杂的指针。
另外,指针最大的危害在于内存泄漏,什么是内存泄漏(Memory Leakage),最为恰当的比喻,就是借钱不还。
//C语言程序
#include <stdio.h>
#include <malloc.h>
int main(int argc,char* argv[])
{
int c = 0;
int* p = (int* )malloc(sizeof(int)); //借钱了
*p = 10;
c = *p;
printf("%d\n",c);
return 0; // 没还钱(没有free)
}
//C++程序
#include <iostream>
using namespace std;
int main(int argc,char* argv[])
{
int* p = new int(10); // 借钱了
int c = 0;
c = *p;
cout << c << endl;
return 0; // 没还钱(没有delete)
}
如此简短的程序,想必读者一定洞若观火,有错是很明显的,解决也是轻而易举,分分钟的事情。但当置身于成千上万行代码的海洋中,无助地感叹,头发又少了!
其实,还钱,90%都是因为忘记了,哈哈对不对?所以花呗自动还款挺好的。高级一点的语言如Java语言,就有内存回收机制,再不担心借钱不还了。虽然C++中没有这样优秀的机制,但是,我们可以充分利用现有的资源来实现目标。很明显我们需要人工的智能,C++中有两个特殊的无返回值的函数会被编译器自动调用,那就是构造函数和析构函数,如果在构造函数(借钱)向堆空间申请内存,在析构函数中(还钱)释放申请的内存不就完美解决问题了吗?
先回顾一下,构造函数与析构函数的用法。
//C++
#include <iostream>
#include <string>
using namespace std;
class Employee
{
private:
string m_name;
public:
Employee(const string& name) // 构造函数
{
m_name = name;
cout << m_name << endl;
}
~Employee() // 析构函数
{
cout << m_name << endl;
}
};
int main(int argc,char* argv[])
{
Employee ep1("xiaoming"); // 创建了一个对象,构造函数自动被调用
return 0; // 程序结束时,析构函数被调用
}
要实现智能指针,还有一个关键要素,那就是操作符重载,简单来说就是把系统中预定义操作符如+ - * / = 等进行重载,当然啦,在C++中还有另外一个重要的重载概念,函数重载,函数重载有待下回分解。指针操作符就两个,* , -> 。激动人心的时刻来了,待我疾速实现一个智能指针。
#include <iostream>
#include <string>
using namespace std;
class Test // 实际要操作的指针对象
{
private:
string m_name;
public:
Test(const string s) // 构造函数里输出调试信息
{
m_name = s;
cout << "My name is " << m_name << endl;
}
Test(const Test& obj)
{
m_name = obj.m_name;
}
Test& operator = (const Test& obj) // 赋值符重载,返回对象的引用,目的是不改变赋值符原义
{
if( this != &obj ) // 判断是否为同一个对象
{
m_name = obj.m_name;
}
}
void print_name()
{
cout << m_name << endl;
}
~Test()
{
cout << "Goodbye " << m_name << endl;
} // 析构函数也输出调式信息
};
class Smart_pointer // 真正的智能指针
{
private:
Test* pt; // 把要使用对象作为私有成员
public:
Smart_pointer(Test* p = NULL)// 使用时在堆空间创建一个对象作为参数
{
pt = p;
}
Smart_pointer(const Smart_pointer& obj)
{
this->pt = obj.pt; // 堆空间使用权力的转移
const_cast<Smart_pointer&>(obj).pt = NULL; // 真正的转移,把之前的置空
}
Smart_pointer& operator = (const Smart_pointer& obj)
{
if( this != &obj )
{
this->pt = obj.pt;
const_cast<Smart_pointer&>(obj).pt = NULL;
}
}
Test* operator -> () // 指针操作符重载,返回对象的地址
{
return pt;
}
Test& operator * () // 指针操作符重载,返回要使用的对象,并且不改变其原义
{
return *pt;
}
bool isNULL() // 判断指针是否为空
{
return ( this == NULL );
}
~Smart_pointer()
{
delete pt; // 析构函数中释放堆空间的内容
} // 自动内存管理的重要手段
};
int main(int argc,char* argv[])
{
Smart_pointer spt(new Test("Sophire"));
spt->print_name(); // 使用方式与指针相同
Test t(*spt);
return 0;
}
天马行空,非常巧妙地利用了C++中的优秀特性进而实现了智能指针。智能指针是大型C++程序中自动内存管理的重要手段,例如Android的framework层就大量使用智能指针,还有Qt平台也提供了QPointer类,STL中也有丰富的智能指针类提供,比如auto_ptr。
上面的智能指针太过专一,问题很大。要是每一个对象都创建一个智能指针类对象来封装,那得多少的代码量,而且绝大部分都是用CTRL+C和CTRL+V,没什么技术含量啊。Well,是时候引入新技能了-------大名鼎鼎的模板(template),将模板一分为二,各自占山为王,一个叫函数模板,另一个叫类模板,很明显智能指针类毫无疑问是用类模板技术,函数模板下回讲解,现在就对类模板一探究竟吧。。。等等等,等,猛攻不如巧夺,先速来回顾一下有异曲同工之妙的宏定义。
#include <iostream>
// 使用宏定义并不需要考虑参数类型
#define Compare(a,b) \
{ \
a > b ? a : b; \
}
using namespace std;
int main(int argc,char* argv[])
{
int a = 1;
int b = 2;
double c = 3;
double d = 4;
int x = Compare(a,b); // 可以处理int类型
double y = Compare(c,d); // 可以处理double类型
cout << "x = " << x << endl;
cout << "y = " << y << endl;
return 0;
}
宏定义天生强大,很多Hackers都倾向于使用宏定义,因为其速度极快,不需要函数调用栈的开销,但是宏的缺点也是致命的,宏定义是由预处理器处理的单元,编译器并不知道宏的存在,所以并不会对其进行语法检查。我们需要的是更安全的方法------模板。直接上代码!
//SmartPointer.h
#ifndef _SMARTPOINTER_H_
#define _SMARTPOINTER_H_
template // 模板技术,代码复用的重要手段
<typename T> // 泛型编程,使用类模板
class SmartPointer
{
private:
T* mp; // T代表了使用时指定的类型
public:
SmartPointer(T* p = NULL)
{
mp = p;
}
SmartPointer(const SmartPointer<T>& obj)
{
mp = obj.mp;
const_cast<SmartPointer<T>&>(obj).mp = NULL;
}
SmartPointer& operator = (const SmartPointer<T>& obj)
{
if (this != &obj)
{
delete mp;
mp = obj.mp;
const_cast<SmartPointer<T>&>(obj).mp = NULL;
}
return *this;
}
T* operator -> ()
{
return mp;
}
T& operator * ()
{
return *mp;
}
T* get()
{
return mp;
}
bool isNULL()
{
return (mp == NULL);
}
~SmartPointer()
{
delete mp;
}
};
#endif
类模板中的函数也可以在类外实现,不过同样需要加上模板的声明。此处不做详解。
现在可以来使用类模板了,实践出真知,动手吧!
//main.cpp
#include <iostream>
#include <string>
#include "SmartPointer.h" // 包含对应的头文件
using namespace std;
class Test // 示例类,可以有多个
{
private:
string m_name;
public:
Test(const string name)
{
m_name = name;
cout << "Hello " << m_name << endl;
}
Test(const Test& obj)
{
cout << "Test(const Test& obj)" << endl;
this->m_name = obj.m_name;
}
void print()
{
cout << "I am " << m_name << endl;
}
~Test()
{
cout << "Goodbye " << m_name << endl;
}
};
int main(int argc, char const *argv[])
{
SmartPointer<Test> pt(new Test("Sophire")); // 在堆空间上创建Test类对象
cout << "pt = " << pt.get() << endl; // 调用pt对象中的函数,获取其所占用堆空间的地址
pt->print(); // 可以像类指针一样操作
cout << endl;
SmartPointer<Test> ptt(pt); // 用刚才的对象初始化新创建的智能指针类对象,发生堆空间使用权力的转移,之前的对象将被置空
cout << "pt = " << pt.get() << endl; // 值为 NULL
cout << "ptt = " << ptt.get() << endl;
ptt->print();
return 0;
}
学无止境,模板有太多的细节值得去深究,例如,模板有一种特殊的实现,模板的特化,这里面的内容相对深奥,知识点较多,读者有兴趣可以自行查阅资料。
1]: http://meta.math.stackexchange.com/questions/5020/mathjax-basic-tutorial-and-quick-reference
[2]: https://mermaidjs.github.io/
[3]: https://mermaidjs.github.io/
[4]: http://adrai.github.io/flowchart.js/