C++ 构造函数与析构函数
类的基本组合元素。
构造函数、析构函数、拷贝构造函数和拷贝复制符
构造函数在对象被创建的时候调用,如:
1 2 3 4 5 6 7 8 9
| class A { };
int main() { A obj; A* p = new A; }
|
析构函数在对象被销毁时刻调用,如:
1 2 3 4 5 6 7 8 9
| int main() { { A obj; }
A* p = new A; delete p; }
|
构造与析构函数的具体讲解可见 类内默认成员函数
面对对象的简介
对象有三个特点: 封装、继承和多态。
封装
封装方法
当我们在使用对象时,自然而然可以把一系列方法放在一个类内,就比如我们想要定义一系列读取 Json
字符串的方法。
下面是 C 语言
的封装方法:
1 2 3 4
| GJSON* gos_json_init(void); void gos_json_free(GJSON* Json); bool gos_json_parse(GJSON* Json, char *szJson); char* gos_json_get_string (GJSON* Json, char *szKey);
|
我们推断使用顺序是:
- 使用
gos_json_init
来获取一个可用的 Json
解析用的结构体, 其中存储了一些信息。
- 使用
gos_json_parse
来读取一系列 Json
字符串中的键值。
- 使用
gos_json_get_string
来通过键来获取值。
- 最后使用
gos_json_free
来释放资源。
那么调用过程如以下代码:
1 2 3 4 5 6 7 8
| int main() { GJSON* pJson = gos_json_init(); gos_json_parse(pJson, "Json string"); char* szValue = gos_json_get_string(pJson, "Key"); ... gos_json_free(pJson); }
|
对比面对对象接口:
1 2 3 4 5 6 7 8 9
| class Json { public: bool parse(char *szJson); char* get_string (char *szKey);
private: GJSON* m_Json; };
|
如上面所示: C++
的接口
由于类内保存了一个 GJSON
的指针 m_Json
, 所以接口函数不需要 GJSON*
的入参.
由于可以被调用的函数只有两个,那我们可以推测调用方法:
- 使用
parser
接口函数解析 json
字符串
- 使用
get_string
接口函数来获取对应键的值
调用如下:
1 2 3 4 5 6
| int main() { Json obj; obj.parser("Json string"); char* szValue = obj.get_string("Key"); }
|
由上面的调用可以看到少了调用 init
和 free
两个函数的过程,因为 class
可以使用构造函数中初始化自己,在析构函数中做相反动作,我们下面补充构造函数和析构函数的定义。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| class Json { public: Json() { m_Json = init(); }
~Json() { free(m_Json); }
bool parse(char *szJson); char* get_string (char*szKey);
private: GJSON* m_Json; GJSON* init(); void free(GJSON* Json); };
|
聚合数据
class
带来的好处是,类内不仅可以定义函数,也可以聚合成员,定义在一起方便查看与传递。
例如我们有一堆配置项数据需要保存。
C 语言可以这样写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| unsigned g_ulLogLevel; bool g_bLogToStdout; bool g_bLogToFile;
void GetLogCfg() { g_ulLogLevel = 1; g_bLogToStdout = TRUE; g_bLogToFile = TRUE; }
extern unsigned g_ulLogLevel; extern bool g_bLogToStdout; extern bool g_bLogToFile;
|
C++ 可以这样写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| class LocalCfg { public: LocalCfg() { GetLogCfg(); }
unsigned m_ulLogLevel; bool m_bLogToStdout; bool m_bLogToFile;
private: void GetLogCfg(); };
int main() { LogCfg obj;
obj.m_ulLogLevel; obj.m_bLogToStdout; obj.m_bLogToFile; }
|
另外我们经常在类中见到函数 Get
和 Set
1 2 3 4 5 6 7 8
| class LocalCfg { public: void Set(int i) { value = i; } int Get() { return value; } private: int value; };
|
我们为什么要把一个简单的赋值操作封装成函数呢?
如果我们想要把变量的赋值与其他业务联动,见下面的例子:
- 追踪赋值,添加打印
1 2 3 4 5 6 7 8 9 10 11 12
| class LocalCfg { public: void Set(int i) { GosLog(LOG_DETAIL, "value: %d -> %d", value, i); value = i; } int Get() { return value; } private: int value; };
|
- 添加锁来支持多线程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| class LocalCfg { public: void Set(int i) { mutex.lock(); value = i; mutex.unlock(); }
int Get() { mutex.lock(); int value_temp = value; mutex.unlock(); return value_temp; }
private: int value; GMutex mutex; };
|
- 业务联动绑定
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
| class LocalCfg { public: void Set(int i) { value = i; IsSet = true; }
int Get() { if (IsSet) { return value; } else { return -1; } }
private: int value; bool IsSet = false; };
|
隐藏细节
见下面代码:
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
| class A { public: bool PutElephantInFreezer() { OpenFreezerDoor(); LetElephantIn(); CloseFreezerDoor(); }
private: void OpenFreezerDoor(); void LetElephantIn(); void CloseFreezerDoor(); };
int main() { A obj; obj.PutElephantInFreezer(); }
|
相关语法介绍
关于 public
与 private
关于关键字 public
和 private
, public
类型的类内成员变量和函数,可以被类的实例调用而 private
不能。
实例化简单来说就是,把一个就是在代码中定义该对象。(如果把类的定义比作蛋糕模子,那么类的实例就是蛋糕)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class OBJECT { public: int i_public; private: int i_private; };
int main() { OBJECT obj;
obj.i_public = 1;
}
|
友元函数介绍 (friend function
)
对于私有变量和私有成员函数, 友元函数可以打破访问权限限制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class A { friend void Set(A& obj, int num); private: int i_private; };
void Set(A& obj, int num) { obj.i_private = num; }
|
友元函数本质是普通函数,友元只是描述的是对类的友元。
友元函数不属于类,是独立的函数,所以不受作用域描述符的限制。
友元函数本身可以同时成为多个类的友元函数。
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
| class B;
class A { friend void Set(A& obja, B& objb, int num); private: int i_private; };
class B { friend void Set(A& obja, B& objb, int num); private: int i_private; };
void Set(A& obja, B& objb, int num) { obja.i_private = num; objb.i_private = num; }
int main() { A obj_a; B obj_b; Set(obj_a, obj_b, 1); return 0; }
|
类内 static
与对象之间的关系
在对象内的 static
变量和函数,与对象的生命周期无关,每一个对象的所有实例都共享同一个 static
变量和函数。
类内 static
函数对类内的静态成员函数、构造函数、析构函数和静态成员变量享有访问权限。
类内静态变量
1 2 3 4 5 6 7 8 9 10 11 12
| class A { public: static int counter = 0; A(){count++;} };
int main() { A obj; std::cout << A::counter << std::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
| class A { public: static int counter; static int GetCounter() { return counter; }
public: int i_non_static = 0; int fun_non_static() { printf("Hello World!"); } };
int A::counter = 0;
int main() { A obj0; A obj1; printf("%d", A::GetCounter()); printf("%d", obj0.GetCounter()); printf("%d", obj1.GetCounter()); return 0; }
|
如上面所示 类内静态成员函数是可以直接访问类内静态成员变量也可以调用类内静态成员函数
但不能调用类内非静态成员变量和函数, 如 i_non_static
、 fun_non_static
静态成员函数的使用限制,不能调用非 static
的类内成员函数和成员变量。
类内静态函数在单例模式中的应用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class Singleton { public: static Singleton& GetInstance() { static Singleon* pInstance = NULL; if(pInstance == NULL) { pInstance = new Singleon; } return *pInstance; }
private: Singleon() {} };
|
继承
继承用来从基类中继承来函数或成员变量, 省却重复定义。
假如我们有很多呼叫相关的业务,都需要一个唯一的业务标识号。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| class base { public: base() : strBusinessID(gos::GetUUID()) {} public: std::string strBusinessID; };
class P2PCall : public base {};
int main() { P2PCall p2p_call;
std::cout << p2p_call.strBusinessID << std::endl; }
|
相关语法介绍
虚析构函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class base { virtual ~base(){ std::cout << "~base" << std::endl; } };
class derive: public base { virtual ~derive() { std::cout << "~derive" << std::endl; } };
int main() { base* p = new derive; delete p; }
|
protected
关键字
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
| class base { private: int i_private; protected: int i_protected; };
class derive : public base { public: int get_protected() { return i_protected; }
};
int main() { base obj0; derive obj; return 0; }
|
多重继承
假设一个程序员又想拥有使用 VSCode
的能力 又想拥有使用 source insight
能力,UML
图如下
写成代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| class VSCode { public: void UseVSCode(); };
class SourceInsight { public: void UseSourceInsight(); };
class Programmer : public VSCode, public SourceInsight { };
int main() { Programmer lijiancong; lijiancong.UseVSCode(); lijiancong.UserSourceInsight(); }
|
如果两个基类中拥有同名成员变量或函数,则派生类使用时应标注该成员变量或函数的作用域,避免产生编译错误。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| class A { protected: int i_protected; };
class B { protected: int i_protected; };
class Derive : public A, public B { public: int get() { return A::i_protected; } };
|
使用 virtual
阻隔菱形继承
我们在使用多重继承时,可能会出现如下的情况。
可能出现如下情况:
菱形继承不仅会出现二义性成员变量名或函数名,而且在虚函数的继承中,中间类每一个类都会保存一个继承的副本,导致未知问题。使用 virtual
关键字避免菱形继承导致的问题。
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
| class Tool { public: Tool() { std::cout << "Tool::i: " << &i << std::endl; } protected: int i = 1; };
class VSCode : public Tool { public: VSCode() { std::cout << "VSCode::i: " << &(VSCode::i) << std::endl; } };
class SourceInsight : public Tool { public: SourceInsight() { std::cout << "SourceInsight::i: " << &(SourceInsight::i) << std::endl; } };
class Programmer : public VSCode, public SourceInsight { };
int main() { Programmer lijiancong; return 0; }
|
如上图, VSCode
和 SourceInsight
两个类都保存了一份基类 Tool::i
的副本, 造成了二义性。使用 virtual
来避免菱形继承带来的问题
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
| class Tool { protected: int i; };
class VSCode : virtual public Tool { };
class SourceInsight : virtual public Tool { };
class Programmer : public VSCode, public SourceInsight { public: int get() { return i; } };
int main() { Programmer lijiancong; lijiancong.get(); }
|
如果上述对象 VSCode
和 SourceInsight
没有使用关键字 virtual
来标注继承方式,那么 Programmer
类中正常使用 Tool::i
。
继承的方式与访问权限
public
、private
、protected
三种继承方式
见基类定义:
1 2 3 4 5 6 7 8 9
| class base { public: int i_public; protected: int i_protected; private: int i_private; };
|
public
继承:
- 基类中
public
成员, 在派生类中表现为 public
- 基类中
protected
成员,在派生类中表现为 protected
- 基类中
private
成员,在派生类中不可访问
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| class derive : public base { public: void get() { i_public = 0; i_protected = 0; } };
int main() { derive obj; obj.i_public = 0; return 0; }
|
protected
继承:
- 基类中
public
成员, 在派生类中表现为 protected
- 基类中
protected
成员,在派生类中表现为 protected
- 基类中
private
成员,在派生类中不可访问
protected
继承与 public
继承相比, 区别在于 基类中 public
成员在 protected
继承后的派生类中降级为 protected
。
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
| class derive : protected base { public: void get() { i_public = 0; i_protected = 0; } };
class derive0 : public derive { void get0() { i_public = 0; i_protected = 0; } };
int main() { derive0 obj; obj.get(); return 0; }
|
private
继承:
- 基类中
public
成员,在派生类中表现为 protected
- 基类中
protected
成员,在派生类中表现为 protected
- 基类中
private
成员,在派生类中不可访问
private
继承与 public
继承相比,区别在于基类中 public
成员和 protected
成员在 private
继承后的派生类中都降级为 private
。
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
| class derive : private base { public: void get() { i_public = 0; i_protected = 0; } };
class derive0 : public derive { void get0() { } };
int main() { derive0 obj; obj.get(); return 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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| class Interface { public: virtual void Insert() = 0; virtual void Query() = 0; };
class MySqlImpl : public Interface { public: virtual void Insert() { MySql_Insert(); }
virtual void Query() { MySql_Query(); } };
class RedisImpl : public Interface { public: virtual void Insert() { Redis_Insert(); }
virtual void Query() { Redis_Query(); } };
class Factory { public: static Interface* getInterface(bool bIsUseMySQL) { if(bIsUseMySQL) { p = new MySqlImpl(); } else { p = new RedisImpl(); } } };
int main() { Factory factory; Interface* p = factory.GetInterface(true); p->Insert(); p->Query(); }
|
上面代码使用多态进行了函数覆盖(override
), 但是在基类和派生类中出现了同名但不同入参的函数名,则会发生函数隐藏(hide
)。
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
| class Interface { public: virtual void Insert(int i) { std::cout << "Interface::Insert" << std::endl; } };
class MySqlImpl : public Interface { public: virtual void Insert(std::string s) { std::cout << "MySQL::Insert" << std::endl; } };
class RedisImpl : public Interface { public: virtual void Insert(int d) { std::cout << "RedisImpl::Insert" << std::endl; } };
int main() { Interface* pRedis = new RedisImpl; pRedis->Insert(1);
Interface* pMySQL = new MySqlImpl; pMySQL->Insert(1); return 0; }
|
使用关键字 override
关键字避免因输入错误而导致函数覆盖不正确的现象。
1 2 3 4 5 6 7 8 9 10 11 12
| class Interface { public: virtual void Insert(int i) { std::cout << "Interface::Insert" << std::endl; } };
class MySqlImpl : public Interface { public: virtual void Insert(std::string s) override { std::cout << "MySQL::Insert" << std::endl; } };
|
虚函数表
一个基类的虚函数,在不同派生类中实现,会产生多个虚函数表。
派生类的多个实例都会保存一个指针,该指针指向对应虚函数的实现(即对应的虚函数表)。
虚函数表中放入特定实现的函数指针,被调用时,通过函数指针来调用对应的汇编。
类成员变量初始化顺序
成员变量在使用初始化列表初始化时,与构造函数中初始化成员列表的顺序无关,只与定义成员变量的顺序有关。因为成员变量的初始化次序是根据变量在内存中次序有关,而内存中的排列顺序早在编译期就根据变量的定义次序决定了.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class a { public: a(): y(0), x(y+1) {} int x; int y; };
int main() { a obj; std::cout << "x: " << obj.x << ", y: " << obj.y; return 0; }
|
输出:
x: -858993459, y: 0