什么是類?
在編程中,類是用來創建對象的模板。可以把類看作一個藍圖,它定義了對象的屬性(特征)和方法(行為)。例如,如果我們有一個“學生”的類,它可能包含學生的名字、年齡等屬性,以及學習、上課等方法。
類的基本結構
類的定義通常是這樣的:
class ClassName {// 類體:由成員函數和成員變量組成
};
- class:這是定義類的關鍵字。
- ClassName:這是類的名字,通常以大寫字母開頭,以便與其他變量區分。
- {}?:大括號內是類的主體,包含類的成員。
- ;?:注意,類定義結束時后面必須有一個分號,這是語法要求,不能省略。
類的成員
類的主體中包含兩種主要的成員:
-
成員變量(屬性)?這些是類中定義的變量,用于存儲對象的狀態。例如,在“學生”類中,可以有?
name
(名字)、age
(年齡)等屬性。 -
成員函數(方法)?這些是類中定義的函數,用于描述對象可以執行的操作。例如,“學生”類可以有?
study()
(學習)和?attendClass()
(上課)等方法。
類的定義方式
定義類時,我們有兩種常見的方式:
聲明和定義全部放在類體中:
class Student {
public:void study() {// 學習的實現}
private:int age; // 年齡
};
- 在這種方式中,所有的成員函數和成員變量都在類的定義內部。這種方式簡單易懂,但編譯器可能會將成員函數當成內聯函數處理。
類聲明放在?.h
?文件中,成員函數定義放在?.cpp
?文件中:
// 在 Student.h 文件中
class Student {
public:void study();
private:int age;
};// 在 Student.cpp 文件中
void Student::study() {// 學習的實現
}
- 這種方式是更常見的做法,特別是在大型項目中。它有助于代碼的組織和管理。
- 當在?
.cpp
?文件中定義成員函數時,函數名前需要加上類名和作用域運算符?::
。
成員變量命名規則的建議
在類中定義成員變量時,命名規則非常重要,尤其是為了區分成員變量與函數參數。以下是一些建議:
class Date {
public:void Init(int year) {// 這里的 year 可能會引起混淆year = year; // 這會導致問題,因為它將參數 year 賦值給自己}
private:int year; // 成員變量
};
為了避免混淆,通常建議使用前綴或后綴來區分成員變量和參數。例如:
-
使用下劃線前綴:
class Date { public:void Init(int year) {_year = year; // 明確區分} private:int _year; // 成員變量 };
-
使用小寫字母 m 作為前綴:
class Date { public:void Init(int year) {mYear = year; // 明確區分} private:int mYear; // 成員變量 };
這些命名規則有助于提高代碼的可讀性,減少錯誤的可能性。具體的命名約定可能會因公司或團隊的要求而有所不同,但通常都建議使用某種前綴或后綴來明確區分成員變量和其他變量。
訪問限定符
在C++中,類的訪問限定符用于控制類成員(屬性和方法)在類外的可見性和訪問權限。通過使用訪問限定符,我們可以實現封裝的特性,讓對象的內部狀態和實現細節不被外部直接訪問。
訪問限定符的類型
public:
用public
修飾的成員可以在類外直接被訪問。這意味著任何地方的代碼都可以使用這些成員。
class Dog {
public:void bark() {std::cout << "Woof!" << std::endl;}
};
protected?和?private:
protected
和private
修飾的成員在類外不能直接被訪問。它們的作用是隱藏類的內部細節。protected
成員可以在派生類中訪問,而private
成員只能在定義它的類內部訪問。
class Dog {
private:int age; // 只能在Dog類內部訪問protected:void wagTail() { // 可以在Dog類和其派生類中訪問std::cout << "Wagging tail!" << std::endl;}
};
作用域:
- 訪問權限的作用域從訪問限定符出現的位置開始,到下一個訪問限定符出現為止。如果沒有后續的訪問限定符,作用域直到類的結束。
默認訪問權限:
- 在C++中,如果沒有顯式指定訪問權限,
class
的默認訪問權限為private
,而struct
的默認訪問權限為public
。這是因為struct
需要兼容C語言的特性。
【面試題】 問題:C++中struct和class的區別是什么?
解答:C++需要兼容C語言,所以C++中struct可以當成結構體使用。另外C++中struct還可以用來 定義類。和class定義類是一樣的,區別是struct定義的類默認訪問權限是public,class定義的類 默認訪問權限是private。
封裝
封裝是面向對象編程的一個重要特性,它將數據(屬性)和操作數據的方法(行為)結合在一起,隱藏內部實現細節,僅公開必要的接口供外部使用。
封裝的例子
想象一下計算機的使用,用戶只需通過開關機鍵、鍵盤和鼠標與計算機進行交互,而不需要了解內部的硬件如何工作。計算機廠商通過外殼隱藏了復雜的內部結構,只提供簡單的操作接口。
在C++中,封裝通過類實現。我們可以將數據和操作數據的方法結合在一起,通過訪問權限控制哪些方法可以被外部訪問。
類的作用域
類定義了一個新的作用域,類的所有成員都在這個作用域內。當我們在類體外定義成員函數時,需要使用作用域操作符?::
?來指明成員屬于哪個類。例如:
class Person {
public:void PrintPersonInfo();
};void Person::PrintPersonInfo() {std::cout << "Person info" << std::endl;
}
類的定義與實例化
類本身并不占用內存空間,它定義了對象的結構和行為。可以把類看作一個藍圖或模板,描述了對象應該包含哪些數據(成員變量)和可以執行哪些操作(成員函數)。
類的比喻
-
學生信息表:想象一個學生信息表,這個表格可以看作一個類,定義了學生的姓名、年齡、性別等屬性。這個表格本身不占用數據,只是一個結構,實際的學生信息需要填寫在這個表格中。
-
謎語的比喻:類可以被看作是一個謎語,而這個謎語的答案(謎底)就是一個具體的實例。比如,“年紀不大,胡子一把,主人來了,就喊媽媽”這個謎語的謎底是“山羊”。這里,“山羊”就是謎語的實例,而謎語本身則是描述“山羊”的類。
類的實例化
類的實例化是創建對象的過程。通過實例化,我們可以根據類的定義創建多個對象,每個對象都有自己的屬性和狀態。
實例化的過程
-
定義類:首先,我們定義一個類,比如
Person
類,描述一個人的屬性和行為。class Person { public:int age; // 年齡void greet() {std::cout << "Hello!" << std::endl;} };
-
創建對象:然后,我們根據
Person
類創建一個或多個對象。int main() {Person person1; // 實例化一個對象person1.age = 25; // 設置屬性person1.greet(); // 調用方法Person person2; // 再實例化一個對象person2.age = 30; // 設置不同的屬性person2.greet(); // 調用方法return 0; }
物理空間的占用
在這個例子中,雖然Person
類本身并不占用內存,但person1
和person2
對象會占用實際的內存空間。每個對象都有自己的age
屬性,存儲了不同的值。
類與對象的比喻
類的實例化可以通過以下比喻來幫助理解:
-
建筑設計圖:類就像是建筑設計圖,描述了建筑的結構和組成部分。設計圖本身并不占用空間,但根據設計圖建造的房子(對象)才是實際存在的。每個房子都是根據同一設計圖建造的,但每個房子都可以有不同的顏色、大小和裝飾。
-
工廠與產品:可以將類視為工廠的藍圖,定義了生產特定類型產品的標準。工廠本身不生產任何產品,但它能根據藍圖生產出多個相同或不同的產品,每個產品都有自己的特性和狀態。
類對象模型
計算類對象的大小
類的大小主要由其成員變量的大小決定,而不包括成員函數。計算機會將成員變量存儲在對象中,而成員函數只會存在一份在代碼段中。
結構體內存對齊規則
內存對齊是為了提高訪問效率。規則如下:
- 第一個成員的地址偏移量為0。
- 其他成員變量要對齊到某個數字的整數倍地址。
- 結構體總大小為最大對齊數的整數倍。
this指針
this
指針是C++中一個隱含的指針,指向當前對象。當成員函數被調用時,this
指針自動傳遞給函數,指向調用該函數的對象。
this
指針的特性
this
指針的類型:
this
指針的類型是類類型* const
,這意味著它是一個指向當前對象的指針,并且在成員函數內部不能改變this
指針的指向。換句話說,你不能讓this
指針指向其他對象。
只能在成員函數內部使用:
this
指針是在成員函數中隱式存在的。你不能在類的外部或靜態成員函數中使用this
指針,因為它僅與特定的對象實例相關聯。
this
指針的本質:
this
指針實際上是成員函數的第一個隱含參數。當對象調用成員函數時,編譯器會將對象的地址作為實參傳遞給this
指針。因此,類的對象并不在自身中存儲this
指針。
this
指針的傳遞:
- 在大多數情況下,
this
指針是由編譯器通過特定寄存器(如x86架構下的ecx
寄存器)自動傳遞的,用戶無需顯式傳遞。
面試題
1.?this
指針存在哪里?
this
指針存儲在棧中。當一個對象調用成員函數時,創建一個新的棧幀,this
指針會作為該棧幀的一部分存在。每次調用成員函數時,this
指針的值會被設置為調用該函數的對象的地址。
2.?this
指針可以為空嗎?
在正常情況下,this
指針不應該為空。this
指針指向當前對象的地址,如果在成員函數中使用了空指針調用該函數,程序會崩潰,通常會導致訪問違規。但在某些情況下,例如在類的靜態成員函數中,this
指針是不可用的,因為靜態成員函數不依賴于任何特定的對象實例。
默認構造函數的生成
在C++中,如果一個類沒有顯式定義構造函數,編譯器會自動生成一個無參的默認構造函數。這個默認構造函數的主要作用是初始化對象的成員變量。
重要特性
- 自動生成:如果用戶沒有定義任何構造函數,編譯器會生成一個無參構造函數。
- 顯式定義的影響:一旦用戶顯式定義了構造函數(無論是無參的還是帶參的),編譯器將不再生成默認構造函數。
示例代碼分析
以下是一個Date
類的示例,展示了無參構造函數和帶參構造函數的使用:
#include <iostream>
using namespace std;class Date {
public:// 無參構造函數Date() {_year = 1900; // 默認值_month = 1;_day = 1;}// 帶參構造函數Date(int year, int month, int day) {_year = year;_month = month;_day = day;}void Print() {cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;
};void TestDate() {Date d1; // 調用無參構造函數d1.Print(); // 輸出: 1900-1-1Date d2(2015, 1, 1); // 調用帶參構造函數d2.Print(); // 輸出: 2015-1-1// 注意:如果通過無參構造函數創建對象時,對象后面不用跟括號,否則就成了函數聲明Date d3(); // 這不是創建對象,而是聲明了一個返回類型為Date的函數
}int main() {TestDate();return 0;
}
構造函數是一個特殊的成員函數,名字與類名相同,創建類類型對象時由編譯器自動調用,以保證 每個數據成員都有 一個合適的初始值,并且在對象整個生命周期內只調用一次。
默認構造函數的作用
很多人可能會質疑,編譯器生成的默認構造函數有什么用。確實,當對象的成員是基本類型(如int
或char
)時,使用默認構造函數不會初始化這些成員,導致它們的值是隨機的。
內置類型與自定義類型
C++將類型分為內置類型(基本類型)和自定義類型(用戶定義的類型)。內置類型在沒有顯式初始化的情況下,其值是未定義的,而自定義類型的成員會調用其默認構造函數進行初始化。
class Time {
public:Time() {_hour = 0;_minute = 0;_second = 0;cout << "Time()" << endl;}
private:int _hour;int _minute;int _second;
};class Date {
private:// 基本類型(內置類型)int _year; // 未初始化,值是隨機的int _month; // 未初始化,值是隨機的int _day; // 未初始化,值是隨機的// 自定義類型Time _t; // 調用Time的默認構造函數
};int main() {Date d; // 創建Date對象,_t會被初始化return 0;
}
C++11的改進
在C++11中,可以在類的聲明中為內置類型的成員變量提供默認值。這確保了即使使用默認構造函數,內置類型的成員變量也會被初始化。
class Date {
private:// 基本類型(內置類型)并提供默認值int _year = 1970; // 初始化為1970int _month = 1; // 初始化為1int _day = 1; // 初始化為1Time _t; // 自定義類型,調用Time的默認構造函數
};int main() {Date d; // 創建Date對象,所有成員都被初始化return 0;
}
編譯時的注意事項
在使用構造函數時,用戶需要注意以下幾點:
- 如果定義了帶參數的構造函數,程序將無法使用默認構造函數,除非顯式定義一個。
- 在聲明對象時,使用“無參數構造函數”時,后面不要加括號,否則會被解釋為函數聲明。
構造函數體賦值
在 C++ 中,構造函數用于在創建對象時為其成員變量提供合適的初始值。不過需要注意的是,構造函數體內的賦值語句并不被稱為成員變量的初始化。只能將?構造函數體內的賦值
?稱為給成員變量?賦初值
。初始化是一個特定的過程,只能執行一次,而賦值可能在構造過程中執行多次。
初始化列表
初始化列表是 C++ 構造函數中用來初始化成員變量的一種方式。它提供了一種語法,使得成員變量可以在構造函數體執行前就被初始化,從而避免不必要的默認構造和然后再賦值的開銷。
以一個冒號開始,接著是一個以逗號分隔的數據成員列表,每個"成員變量"后面跟一個放在括 號中的初始值或表達式。
class Date {
public:Date(int year, int month, int day): _year(year), _month(month), _day(day) {}private:int _year;int _month;int _day;
};
如上所示,在初始化列表中,成員變量?_year
、_month
?和?_day
?在構造函數體執行之前就被賦予了初始值。
初始化列表使用須知
- 每個成員變量只能在初始化列表中出現一次(初始化只能初始化一次)。
- 必須在初始化列表中初始化的成員:
- 引用成員變量
const
?成員變量- 自定義類型成員(且該類沒有默認構造函數時)
- 優先使用初始化列表,尤其對于自定義類型的成員變量,初始化列表能夠確保他們以最快的方式得到初始化,避免默認構造調用。
成員變量的初始化順序
成員變量在類中聲明的順序決定了它們在初始化列表中的初始化順序。無論在初始化列表中出現的順序如何,實際的初始化順序將按照成員聲明的順序進行。
class A {
public:A(int a): _a1(a), _a2(_a1) {} // _a2 會使用 _a1初始化void Print() {std::cout << _a1 << " " << _a2 << std::endl;}
private:int _a1;int _a2;
};
示例代碼分析
int main() {A aa(1);aa.Print(); // 輸出結果分析
}
對于上述代碼,構造?A
?的對象時,_a1
?初始化為?1
。接下來?_a2
?將會使用?_a1
?的值初始化。由于在此時?_a1
?已經是?1
,所以?_a2
?被賦值為?1
。
因此,輸出將是:
1 1
析構函數
概念
析構函數是與構造函數相反的特殊成員函數。當對象的生命周期結束時,析構函數會被自動調用。析構函數的主要作用是清理對象使用的資源,例如動態分配的內存、打開的文件、網絡連接等。
對象在銷毀時會自動調用析構函數,完成對象中資源的清理工作。
特性
析構函數具有以下特性:
- 命名規則:析構函數的名稱是在類名前加上字符
~
(波浪號)。例如,class MyClass { ~MyClass(); };
。 - 無參數和無返回值:析構函數不接受參數,并且沒有返回值類型。
- 唯一性:一個類只能有一個析構函數。如果未顯式定義,編譯器會自動生成一個默認的析構函數。
- 自動調用:當對象的生命周期結束時,C++編譯器會自動調用析構函數。
示例代碼
以下是一個使用析構函數的示例,展示了如何在類中管理動態分配的內存:
#include <iostream>
#include <cstdlib> // malloc, free
using namespace std;typedef int DataType;class Stack {
public:Stack(size_t capacity = 3) {_array = (DataType*)malloc(sizeof(DataType) * capacity);if (NULL == _array) {perror("malloc申請空間失敗!!!");return;}_capacity = capacity;_size = 0;}void Push(DataType data) {if (_size < _capacity) {_array[_size] = data;_size++;} else {cout << "Stack is full!" << endl;}}// 析構函數~Stack() {if (_array) {free(_array);_array = NULL;_capacity = 0;_size = 0;cout << "Stack memory freed!" << endl;}}private:DataType* _array;int _capacity;int _size;
};void TestStack() {Stack s;s.Push(1);s.Push(2);
}int main() {TestStack(); // 當TestStack結束時,Stack對象s被銷毀,析構函數被調用return 0;
}
在這個示例中,Stack
類包含一個動態分配的數組_array
。在析構函數中,使用free
釋放了分配的內存,確保不會發生內存泄漏。
編譯器生成的析構函數
如果一個類中包含自定義類型的成員變量,編譯器生成的默認析構函數會自動調用這些自定義類型成員的析構函數。以下是一個示例:
class Time {
public:~Time() {cout << "~Time()" << endl;}
private:int _hour;int _minute;int _second;
};class Date {
private:int _year = 1970; // 內置類型int _month = 1; // 內置類型int _day = 1; // 內置類型Time _t; // 自定義類型
};int main() {Date d; // 創建Date對象return 0; // 當d的生命周期結束時,調用Date的析構函數,進而調用Time的析構函數
}
輸出分析
當Date
對象d
的生命周期結束時,編譯器會自動調用Date
的析構函數(如果沒有顯式定義,則使用默認析構函數)。在這個過程中,Time
類的析構函數也會被調用,盡管在main
函數中沒有直接創建Time
對象。
重要注意事項
- 析構函數的調用:創建哪個類的對象,銷毀時調用的就是該類的析構函數。即使在類中沒有顯式定義析構函數,編譯器也會自動生成一個,以確保所有成員(特別是自定義類型)都能正確釋放資源。
- 內置類型的處理:對于內置類型的成員變量,析構函數不需要進行特殊處理,因為它們在對象銷毀時會自動釋放內存。
- 無法重載:一個類只能有一個析構函數,且無法重載。
- 如果類中沒有申請資源時,析構函數可以不寫,直接使用編譯器生成的默認析構函數,比如 Date類;有資源申請時,一定要寫,否則會造成資源泄漏,比如Stack類。
拷貝構造函數的概念
拷貝構造函數是一個特殊的構造函數,用于通過已存在的對象創建一個新對象。它的主要作用是初始化新對象,使其與傳入的對象具有相同的狀態。
拷貝構造函數的特性
拷貝構造函數具有以下特性:
- 構造函數的重載形式:拷貝構造函數是構造函數的一個重載形式。
- 單個參數:它的參數只有一個,且必須是本類類型對象的引用,通常使用
const
修飾。這是因為如果使用值傳遞,會導致無限遞歸調用。 - 編譯器生成的默認拷貝構造函數:如果未顯式定義拷貝構造函數,編譯器會生成一個默認的拷貝構造函數。默認的拷貝構造函數會按字節進行拷貝,這種拷貝稱為淺拷貝。
示例代碼
以下是一個Date
類和Time
類的示例,展示了如何使用拷貝構造函數:
#include <iostream>
using namespace std;class Time {
public:Time() {_hour = 1;_minute = 1;_second = 1;}// 拷貝構造函數Time(const Time& t) {_hour = t._hour;_minute = t._minute;_second = t._second;cout << "Time::Time(const Time&)" << endl;}private:int _hour;int _minute;int _second;
};class Date {
public:Date(int year = 1970, int month = 1, int day = 1): _year(year), _month(month), _day(day) {}// 拷貝構造函數Date(const Date& d) {_year = d._year;_month = d._month;_day = d._day;_t = d._t; // 調用Time的拷貝構造函數}private:int _year;int _month;int _day;Time _t; // 自定義類型
};int main() {Date d1; // 創建d1對象Date d2(d1); // 使用拷貝構造函數創建d2對象return 0;
}
在這個示例中,Date
類的拷貝構造函數會在創建d2
時被調用,而Time
類的拷貝構造函數也會在Date
類的拷貝構造函數中被調用。
淺拷貝與深拷貝
如果類中包含指針或動態分配的內存,編譯器生成的默認拷貝構造函數會執行淺拷貝。淺拷貝會導致多個對象指向同一塊內存,這可能會導致資源管理的問題,比如雙重釋放內存。
示例:淺拷貝導致的問題
#include <iostream>
#include <cstdlib> // malloc, free
using namespace std;class Stack {
public:Stack(size_t capacity = 10) {_array = (DataType*)malloc(capacity * sizeof(DataType));if (nullptr == _array) {perror("malloc申請空間失敗");return;}_capacity = capacity;_size = 0;}~Stack() {if (_array) {free(_array);_array = nullptr;}}// 拷貝構造函數(未定義,使用默認的淺拷貝)// Stack(const Stack& s) = default; // 如果顯式定義為default,編譯器會自動生成private:DataType* _array;size_t _size;size_t _capacity;
};int main() {Stack s1; // 創建s1對象Stack s2(s1); // 使用拷貝構造函數創建s2對象(淺拷貝)return 0; // 當程序結束時,s1和s2的析構函數都會被調用,可能導致雙重釋放內存
}
注意:類中如果沒有涉及資源申請時,拷貝構造函數是否寫都可以;一旦涉及到資源申請 時,則拷貝構造函數是一定要寫的,否則就是淺拷貝。
拷貝構造函數的使用場景
拷貝構造函數通常用在以下場景中:
- 使用已存在對象創建新對象:例如,
Date d2(d1);
。 - 作為函數參數:如果函數的參數是類類型對象,通常會使用拷貝構造函數。
- 作為函數返回值:返回類類型對象時,拷貝構造函數會被調用。
為了提高程序效率,通常在傳遞對象時使用引用類型,返回值時根據實際場景決定使用值返回還是引用返回。
運算符重載
C++引入運算符重載的目的是為了增強代碼的可讀性和可維護性。運算符重載允許程序員為自定義數據類型定義特定的運算符行為,其實質是在類中定義具有特殊名稱的函數,這些函數的名稱是由關鍵字?operator
?加上需要重載的運算符符號構成的。運算符重載函數的返回值類型和參數列表與普通函數相似。
運算符重載的基本格式
函數原型如下:
返回值類型 operator 操作符(參數列表);
在運算符重載時,有幾個重要的注意事項:
- 運算符重載不能通過連接其他符號來創建新的操作符,例如?
operator@
?是不允許的。 - 所有被重載的操作符必須至少有一個參數是類類型。
- 對于內置類型的運算符,例如整型?
+
,其原有含義不能被改變。 - 當作為類成員函數進行重載時,運算符的參數數量往往比操作數少1,因為第一個參數是隱含的?
this
?指針。 - 有五個運算符是不能被重載的:
.*
、::
、sizeof
、?:
?和?.
。
示例:全局的等于運算符重載
以下為?Date
?類的示例,演示全局運算符重載?==
:
class Date {
public:Date(int year = 1900, int month = 1, int day = 1) : _year(year), _month(month), _day(day) {}friend bool operator==(const Date& d1, const Date& d2) {return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;}private:int _year;int _month;int _day;
};void Test() {Date d1(2018, 9, 26);Date d2(2018, 9, 27);std::cout << (d1 == d2) << std::endl; // 輸出 0 (false)
}
在這個示例中,注意到全局運算符重載使得成員變量必須是公有的,因此可能影響類的封裝性。可以通過使用友元函數或將運算符重載定義為成員函數來解決這個問題。
示例:成員函數的等于運算符重載
下面是將運算符重載改為成員函數的示例:
class Date {
public:Date(int year = 1900, int month = 1, int day = 1) : _year(year), _month(month), _day(day) {}bool operator==(const Date& d2) const {return _year == d2._year && _month == d2._month && _day == d2._day;}private:int _year;int _month;int _day;
};
在這個示例中,operator==
?被定義為?Date
?類的成員函數,其中使用?const
?關鍵字修飾,表明該函數不會修改類的任何成員。
賦值運算符重載
賦值運算符重載的格式如下:
- 參數類型:
const T&
,通過引用傳遞以提高效率。 - 返回值類型:
T&
,返回引用以支持鏈式賦值。
賦值運算符重載時需要注意:
- 檢測是否自我賦值。
- 返回?
*this
?以支撐鏈式賦值操作。
示例代碼如下:
class Date {
public:Date(int year = 1900, int month = 1, int day = 1) : _year(year), _month(month), _day(day) {}Date& operator=(const Date& d) {if(this != &d) { // 防止自我賦值_year = d._year;_month = d._month;_day = d._day;}return *this; // 返回自身引用}private:int _year;int _month;int _day;
};
賦值運算符只能作為類的成員函數重載,不能定義為全局函數。這是因為編譯器會自動生成一個默認的賦值運算符,如果用戶再在類外定義一個全局的重載,就會與編譯器生成的函數產生沖突。
自定義運算符重載示例
在?Date
?類中繼續添加其他運算符的重載,例如自增操作符和日期算術運算符:
class Date {
public:Date(int year = 1900, int month = 1, int day = 1) : _year(year), _month(month), _day(day) {}Date& operator++() { // 前置++_day += 1;return *this;}Date operator++(int) { // 后置++Date temp = *this; // 保存當前日期_day += 1;return temp; // 返回變更之前的日期}// 其他運算符重載...private:int _year;int _month;int _day;
};
在上述代碼中,前置和后置自增操作符的重載遵循了相應的規則和約定,我們保證了高效的操作和正確的行為。
日期類的實現
在?main
?函數中,我們可以看到日期類的使用:
int main() {Date d1(2022, 1, 13);Date d = d1++; // d: 2022, 1, 13; d1: 2022, 1, 14d = ++d1; // d: 2022, 1, 15; d1: 2022, 1, 15return 0;
}
這個示例展示了如何利用重載的運算符來進行日期對象的日常操作。
const成員
將const修飾的“成員函數”稱之為const成員函數,const修飾類成員函數,實際修飾該成員函數隱含的this指針,表明在該成員函數中不能對類的任何成員進行修改。
1. const 對象可以調用非 const 成員函數嗎?
答案:不可以。對于常量對象(const
?對象),編譯器確保其狀態不能被修改,因此如果嘗試調用非?const
?成員函數會導致編譯錯誤。這是因為非?const
?成員函數可能改變對象的狀態,而?const
?對象的狀態是不允許被修改的。例如:
const Date d(2022, 1, 13);
d.Print(); // 可以
d.SomeNonConstFunction(); // 錯誤:const 對象不能調用非 const 成員函數
2. 非 const 對象可以調用 const 成員函數嗎?
答案:可以。非?const
?對象可以調用?const
?成員函數。const
?成員函數承諾不修改對象的狀態,因此可以安全地在可變對象上調用它們。例如:
Date d(2022, 1, 13);
d.Print(); // 可以,調用 const 成員函數
3. const 成員函數內可以調用其他非 const 成員函數嗎?
答案:不可以。const
?成員函數不能調用非?const
?成員函數,因為這可能會導致狀態改變,違背了?const
?成員函數的目標。例如:
class Date {
public:void Modify() { _year++; } // 非 const 成員函數void Print() const {Modify(); // 錯誤:不能在 const 成員函數中調用非 const 成員函數}
private:int _year;
};
4. 非 const 成員函數內可以調用其他 const 成員函數嗎?
答案:可以。非?const
?成員函數可以調用?const
?成員函數,因為?const
?成員函數不修改對象狀態,調用是安全的。例如:
class Date {
public:void Print() const {std::cout << "Year: " << _year << std::endl;}void Modify() {Print(); // 可以,因為 Print 是 const 成員函數}
private:int _year;
};
靜態成員的概念
在 C++ 中,使用?static
?關鍵字修飾的類成員被稱為靜態成員,這包括靜態成員變量和靜態成員函數。靜態成員屬于類本身,而不是某個特定的對象,因此它們在所有類的對象之間共享。
實現一個類,計算程序中創建的類對象數量
下面是一個簡單的示例,通過靜態成員變量來記錄對象的數量:
#include <iostream>class ObjectCounter {
public:ObjectCounter() {count++; // 每創建一個對象,計數增加}~ObjectCounter() {count--; // 每銷毀一個對象,計數減少}static int getObjectCount() {return count; // 靜態成員函數可以訪問靜態成員變量}private:static int count; // 聲明靜態成員變量
};// 靜態成員變量在類外進行定義初始化
int ObjectCounter::count = 0; // 初始化為0int main() {ObjectCounter obj1; // count = 1ObjectCounter obj2; // count = 2std::cout << "Current Object Count: " << ObjectCounter::getObjectCount() << std::endl; // 輸出 2{ObjectCounter obj3; // count = 3std::cout << "Current Object Count: " << ObjectCounter::getObjectCount() << std::endl; // 輸出 3} // obj3 被銷毀,count = 2std::cout << "Current Object Count: " << ObjectCounter::getObjectCount() << std::endl; // 輸出 2return 0;
}
靜態成員的特性
- 共享性:靜態成員在所有類的對象之間共享,它們不屬于某個特定的對象,而是存儲在靜態存儲區。
- 類外定義:靜態成員變量必須在類外進行定義,定義時不需要?
static
?關鍵字。 - 訪問方式:靜態成員可以通過?
類名::靜態成員
?或者實例對象訪問?對象.靜態成員
?來訪問。 - 無?
this
?指針:靜態成員函數沒有隱含的?this
?指針,因此它不能訪問任何非靜態成員。 - 訪問權限:靜態成員也受到類的訪問控制(
public
、protected
、private
)的影響。
問題解答
-
靜態成員函數可以調用非靜態成員函數嗎?
?答案:不可以。靜態成員函數不具有?
this
?指針,因此它無法訪問類的非靜態成員函數或非靜態成員變量。如果嘗試在靜態成員函數中調用非靜態成員函數,編譯器將報錯。class Example { public:static void staticFunction() {nonStaticFunction(); // 錯誤: 不能調用非靜態成員}void nonStaticFunction() {std::cout << "Non-static function called." << std::endl;} };
-
非靜態成員函數可以調用類的靜態成員函數嗎?
?答案:可以。非靜態成員函數可以自由地調用類的靜態成員函數,因為非靜態成員函數有?
this
?指針,可以訪問類的所有成員,包括靜態成員。class Example { public:static void staticFunction() {std::cout << "Static function called." << std::endl;}void nonStaticFunction() {staticFunction(); // 可以調用靜態成員函數} };
友元
友元關系在 C++ 中提供了一種突破類封裝的機制,可以讓特定的函數或類訪問類的私有成員。雖然友元可以方便地訪問私有數據,增加了程序的靈活性,但過多使用會導致高耦合,從而損害封裝性。友元可以分為兩種類型:友元函數和友元類。
友元函數
友元函數是定義在類外的普通函數,但為了讓其能夠訪問類的私有和保護成員,需要在類內使用?friend
?關鍵字聲明。
重載?operator<<
在實現重載?operator<<
?時,由于?cout
?是流對象,我們無法將?operator<<
?定義為成員函數,因為成員函數的第一個參數始終是?this
?指針。而?<<
?操作符需要一個流對象作為其第一個參數。因此,我們必須將其定義為全局函數,并使用友元函數來訪問期望的類成員。
#include <iostream>
using namespace std;class Date {friend ostream& operator<<(ostream& _cout, const Date& d);friend istream& operator>>(istream& _cin, Date& d);public:Date(int year = 1900, int month = 1, int day = 1): _year(year), _month(month), _day(day) {}private:int _year;int _month;int _day;
};// 定義友元函數
ostream& operator<<(ostream& _cout, const Date& d) {_cout << d._year << "-" << d._month << "-" << d._day;return _cout;
}istream& operator>>(istream& _cin, Date& d) {_cin >> d._year >> d._month >> d._day;return _cin;
}int main() {Date d; cin >> d; // 輸入示例: 2023 12 25cout << d << endl; // 輸出: 2023-12-25return 0;
}
友元函數的特點
- 友元函數可以訪問類的私有和保護成員,但它不是類的成員函數。
- 友元函數不能用?
const
?修飾。 - 友元函數的聲明可以在類定義的任意位置,且不受類的訪問權限控制。
- 多個類可以共享同一個友元函數。
- 友元函數的調用方式與普通函數相同。
友元類
友元類的所有成員函數可以訪問被聲明為友元的類中的非公有成員。友元關系是單向的,意味著如果類 A 是類 B 的友元,B 的成員可以訪問 A 的私有成員,但反之不成立。
友元關系示例
class Time {friend class Date; // 聲明 Date 為 Time 的友元類public:Time(int hour = 0, int minute = 0, int second = 0): _hour(hour), _minute(minute), _second(second) {}private:int _hour;int _minute;int _second;
};class Date {
public:Date(int year = 1900, int month = 1, int day = 1): _year(year), _month(month), _day(day) {}void SetTimeOfDate(int hour, int minute, int second) {// 直接訪問 Time 類的私有成員_t._hour = hour;_t._minute = minute;_t._second = second;}private:int _year;int _month;int _day;Time _t; // Time 類型的成員變量
};int main() {Date d(2023, 12, 25);d.SetTimeOfDate(10, 30, 45); // 設置時間return 0;
}
友元類的特點
- 單向友元關系:如果類 B 是類 A 的友元,B 可以訪問 A 的私有成員,但 A 不會自動訪問 B 的私有成員。
- 友元關系不可傳遞:如果 B 是 A 的友元,C 是 B 的友元,則 C 并不是 A 的友元。
- 友元關系不能繼承:友元關系不會隨著類的繼承而繼承。
再次理解類與對象
在面向對象編程(OOP)中,類(Class)和對象(Object)是最基本的概念。正確理解這兩個概念是編寫有效和結構良好的程序的基礎。
1. 描述現實世界的抽象
在現實生活中,我們遇到許多具體的實體,比如洗衣機、汽車、學生、員工等。計算機并不直接理解這些真實世界的實體,但可以通過抽象化的方式與這些實體建立聯系。這個過程可以分為幾個步驟:
-
抽象:將對象的關鍵信息和特征提取出來。對于洗衣機,我們可能會考慮其屬性(如品牌、顏色、容量)和方法(如啟動、停止、洗滌、脫水)。
-
定義類:使用編程語言(如 C++、Java、Python 等)將抽象的概念轉變為類。類是一個藍圖,它描述了某種類型的對象的屬性和行為,實際上是對這些對象的定義。
-
實例化對象:類只是一個概念,是一個模板;通過類,我們可以創建具體的對象。每個對象代表一個具體的實體,這些實體可以使用類中的定義的屬性和方法。
-
模擬和操作對象:一旦對象被創建,我們可以通過編寫代碼來模擬現實生活中洗衣機的行為,例如讓其啟動或停止,獲取其當前狀態等。
2. 類與對象的關系
-
類:是定義對象的藍圖,包含了對象的屬性和方法。它描述了對象的性質(數據)和功能(方法)。類本質上是一個自定義類型。
-
對象:是類的實例,是類中屬性和方法的具體實現。每個對象都有自己獨特的狀態,但它們共享類定義的結構和行為。