如何對C++類進行編寫、封裝

C++與C語言面向過程不同,是面向對象的語言。

面向對象的三大特性:封裝、繼承、多態

什麼是封裝?就是要把類變成像內置類型及其複合形式(數組)一樣,能夠直接初始化、複製、各種運算等等。C++中的面向對象就是怎麼把資源一類面向過程的資源轉成像面向對象那樣簡單易用。

資源有類,其中一種是計算機資源。剛開始C編程的時候,那麼是直接跟計算機系統打交道。打開文件或IO,獲得其文件描述符(指向了內核裏的對象/指針),然後通過操作該指針進行操作;多線程之間需要處理同步問題,那麼就用信號量同步。像文件描述符、信號量、鎖、線程等操作系統內核提供的資源,這就是計算機資源。像這類資源,在一個進程中是唯一的,調用了之後需要自己主動釋放資源(例如close、release等)。當我們使用這類資源,就有一個過程,需要開始、中間操作、結束等步驟。那能不能像使用內置變量那麼簡單,要用的時候生成,然後就不用管它釋放與否,讓系統自己去處理?

類要封裝的不僅僅是操作系統資源,還有從heap分配的資源。heap資源是第二種資源,這類資源和系統資源一樣,new了之後,需要手動調用delete去釋放資源。否則,隨着進程長時間運行,這個空間不僅沒有被釋放,就是所謂的內存泄露,而且此類型代碼多次出現,就會不斷地佔用空間,從而導致OOM(out of memory)

資源對象是需要自己管理的,那不用自己管理的對象有嗎?有的,例如stack分配的對象。stack是程序來管理的,這類對象只在起代碼作用域及其子作用域有效。離開了作用域,就會被系統自動回收,無法使用。那麼如果要在不同的函數中調用,那麼就需要定義全局變量。而全局變量是一開始要確定的,但是存在一個問題,就是你永遠不知道你需要定義多少個全局變量才滿足。而要解決運行過程中新分配的空間需求,就需要使用heap分配的方式。這種方式最大的問題就在於需要手動釋放空間,否則就會存在上述內存泄露的問題!

代碼1:內置對象

include<iostream>


int main() {

	for (int i = 0, a = i; i < 5; i++) {
		a = i;
		std::cout << a << std::endl;
	}
	return 0;
}

上面代碼就說明了內置類型對象(非資源對象)是多麼簡單易用。其中的a是main函數分配的int變量,而b是f函數分配的int變量。當main函數開始運行時,那麼系統會在stack分配空間。當main函數結束運行,系統會自動回收stack,自然而然把a空間回收。同理,b變量的空間是由f函數自動分配和回收。

C++類,某種意義上來說是一種作用域、是一種命名空間。而這裏面,會管理資源對象和非資源對象。只有非資源對象的類,是很好編寫的,甚至都不用爲它定義構造函數、析構函數、拷貝構造、賦值構造,直接使用C++編譯器默認提供的就可以了。而對於既有非資源對象、又有資源對象的類和只有資源對象的類,就需要謹慎處理,這也就是Primer C++所說的行爲像指針的類。

同時,C++類面向對象的另一個特點,具有對象的生命週期。生命週期開始,調用構造函數;生命週期結束時,調用析構函數。調用構造函數時生命週期開始(根據std模板編程,需要先分配空間、然後調用構造函數。在stack會自動分配空間,調用對應的構造函數;在heap上會調用operator new,再調用placement new);而生命週期結束,發生在作用域結束(stack自動調用disconstruct)、delete指針所指向的對象(heap上會destructor,再delete空間)和基類析構被子類析構調用。

封裝的重點就是如何把含有資源對象的類進行封裝!

1、從heap分配空間的類,典型的就是string、智能指針、容器類等等;

2、要處理計算機資源的類,典型的包括iostream、thread類等等。

將這類資源對象封裝成類,把所有對象都當作類來使用,這纔是面向對象的封裝的精髓所在。需要注意的是char就是一個字節的變量類型,跟int的區別只有數據大小的區別,而字符串是char數組的表現

代碼2:String類

//具體實現不是這樣的,而是通過模板、容器的方式實現,這裏只是爲了說明

class String {

friend std::ostream& operator<<(std::ostream& os, const String& s);

public:
	String();
	String(const char*);
	String(const String&);
	String& operator=(const String& s);
	~String();
	

private:
	char* str;
	size_t size;
};

String::String() {
	str = new char[1]{ '\0' };
	size = 0;
}

String::String(const char* s) {
	size = strlen(s);
	str = new char[size + 1];
	strcpy(str, s);
}

String::String(const String& s) {
	str = new char[s.size + 1];
	strcpy(str, s.str);
}

String& String::operator=(const String& s) {
	char * new_str = new char[s.size + 1];
	strcpy(new_str, s.str);
	delete[] str;
	str = new_str;

	return *this;
}

String::~String() {
	delete[] str;
	str = nullptr;
}


std::ostream& operator<<(std::ostream& os, const String& s) {
	os << s.str;
	return os;
}

這種類型的類需求很常見,涵蓋了管理資源(行爲類似值一樣)、重載運算符、構造函數和析構函數等多種C++類封裝需要注意的情況。String類和char數組有區別嗎?顯然是沒有區別的(增刪改查都可以、能合併:後者須注意越界判斷)。然而,它們的實現方式是有區別的:前者用new的方式在heap上分配,而後者更簡單直接,用數組解決(stack)。具體體現:1、沒辦法用類封裝一個可以動態改變的數組

代碼3:String2類,不使用資源管理方式的錯誤例子

class String2 {

friend std::ostream& operator<<(std::ostream& os, const String2& s);

public:
	String2();
	String2(const char*);
	String2(const String&);
	String2& operator=(const String2& s);
	~String2();

private:
	char str[10];
	size_t size;
};

String2::String2() {
	str[0] = '\0';
	size = 0;
}

String2::String2(const char* s) {
	size_t ssize = strlen(s);
	if (ssize > 10) {
		// out of size, return failed
	}
	else {
		strcpy(str, s);
	}	
}
//...

編程過程中,String2的大小固定了,就像是String2 = char s[10]。當需要char s[20]的時候,String2顯得無能爲力,因爲String2 = char s[10],除非訂一個String3=char s[20]。這種類用起來還沒有char數組痛快(當然可以用模板來實現StringX)。類的封裝性——有時候不得不用資源對象來更好地封裝一個類。

這句話反過來思考,因爲要便於自動分配和回收heap的資源,所以用了類來封裝。所以某種意義上來說,類就是爲了管理資源的分配而出現的。C裏面你想用heap,可以,但是要自己管理;C++你想用heap,用已有的類調用,不用你管理!C++中明顯用了很多heap相關的東西。

但是有個問題,並不是所有人都通曉資源的特點和語言的特性(C++),還有一些人習慣用C,自己管理資源;有些程序它本身就需要重heap分配跨作用域使用。所以C++提供了智能指針,讓使用者快速構建一個類(最小),將面向過程語言轉成面嚮對象語言。智能指針shared_ptr是行爲類似指針的類,當發生拷貝智能指針對象,引用數會增加。所以說智能指針是快速構建類的方式之一,一旦使用智能指針封裝了,那麼就不用再考慮智能指針對象的資源管理問題,就是一個類似內置類型的變量。

如果要實現行爲類似值的對象,還是用智能指針,這會考慮的是多個不同的智能指針,是new初始化實現的。而行爲像指針是拷貝、賦值出來的。行爲類似值或行爲類似指針,只是考慮的層次不同

總之:

1、不需要管理資源的類,使用默認構造、析構、拷貝和賦值函數;

2、如果設計的類需要管理資源,考慮使用智能指針將資源封裝、轉變成不需要管理資源的類,使用默認拷貝、賦值和析構函數(https://github.com/wenion/modbuscppwrapper/blob/master/Modbus.h);

2、如果設計的類需要管理資源,並且行爲類似值,那麼需要自定義封裝構造、析構、拷貝和賦值函數(例子:上例String)。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章