類設計總結
執行個體一
本執行個體設計了三個類。第一個是基類
Person
類,具有姓名(
m_name
)和年齡(
m_age
)屬性。第二個學生類
Student
公有繼承
Person
類,擁有性别(
m_sex
)屬性。第三個老師類
Teacher
公有繼承
Person
類,擁有學科(
m_subject
)屬性。
Person.h
#ifndef DMA_H_
#define DMA_H_
#include <iostream>
// 基類
class Person
{
private:
char *m_name; // 姓名
int m_age; // 年齡
public:
Person(const char *name = "", int age = 0); // 構造函數
Person(const Person &p); // 複制構造函數
virtual ~Person(); // 虛析構函數
Person& operator=(const Person &p); // 指派運算符
friend std::ostream& operator<<(std::ostream &os, const Person& p); // 友元函數
};
// 派生類
class Student : public Person // 公有繼承
{
private:
enum {SEX_LEN = 10}; // 枚舉
char m_sex[SEX_LEN]; // 性别
public:
Student(const char *name = "", int age = 0, const char *sex = "female"); // 構造函數
Student(const Person &p, const char *sex = "female"); // 構造函數
friend std::ostream& operator<<(std::ostream& os, const Student &stu); // 友元函數
};
// 派生類
class Teacher : public Person // 公有繼承
{
private:
char *m_subject; // 科目
public:
Teacher(const char *name = "", int age = 0, const char *subject = "Chinese"); // 構造函數
Teacher(const Person& p, const char *subject = "Chinese"); // 構造函數
Teacher(const Teacher &tea); // 複制構造函數
Teacher& operator=(const Teacher &tea); // 指派運算符
virtual ~Teacher(); // 析構函數
friend std::ostream& operator<<(std::ostream& os, const Teacher& tea); // 友元函數
};
#endif
Person.cpp
#include "Person.h"
#include <cstring>
Person::Person(const char *name, int age)
{
m_name = new char[strlen(name) + 1];
strcpy(m_name, name);
m_age = age;
}
Person::Person(const Person& p)
{
m_name = new char[strlen(p.m_name) + 1];
strcpy(m_name, p.m_name);
m_age = p.m_age;
}
Person::~Person()
{
delete [] m_name;
m_name = nullptr;
}
Person& Person::operator=(const Person& p)
{
if(this == &p)
{
return *this;
}
delete [] m_name;
m_name = new char[strlen(p.m_name) + 1];
strcpy(m_name, p.m_name);
m_age = p.m_age;
return *this;
}
std::ostream& operator<<(std::ostream &os, const Person& p)
{
os << "name: " << p.m_name << std::endl;
os << "age: " << p.m_age << std::endl;
return os;
}
Student::Student(const char *name, int age, const char *sex)
: Person(name, age) // 初始化成員清單
{
strncpy(m_sex, sex, SEX_LEN - 1);
m_sex[SEX_LEN - 1] = '\0';
}
Student::Student(const Person& p, const char *sex)
: Person(p)
{
strncpy(m_sex, sex, SEX_LEN - 1);
m_sex[SEX_LEN - 1] = '\0';
}
std::ostream& operator<<(std::ostream& os, const Student &stu)
{
os << (const Person &)stu; // 強轉成基類,輸出基類部分的内容
os << "Sex: " << stu.m_sex << std::endl;
return os;
}
Teacher::Teacher(const char *name, int age, const char *subject)
: Person(name, age)
{
m_subject = new char[strlen(subject) + 1];
strcpy(m_subject, subject);
}
Teacher::Teacher(const Person& p, const char *subject)
: Person(p)
{
m_subject = new char[strlen(subject) + 1];
strcpy(m_subject, subject);
}
Teacher::Teacher(const Teacher &tea)
: Person(tea) // 使用基類的複制構造函數
{
m_subject = new char[strlen(tea.m_subject) + 1];
strcpy(m_subject, tea.m_subject);
}
Teacher& Teacher::operator=(const Teacher &tea)
{
if(this == &tea)
{
return *this;
}
Person::operator=(tea); // 調用基類的指派運算符,給基類部分的成員指派
delete [] m_subject;
m_subject = new char[strlen(tea.m_subject) + 1];
strcpy(m_subject, tea.m_subject);
return *this;
}
Teacher::~Teacher()
{
delete [] m_subject;
m_subject = nullptr;
}
std::ostream& operator<<(std::ostream& os, const Teacher& tea)
{
os << (const Person&)tea; // 強轉成基類,輸出基類部分的内容
os << "Subject: " << tea.m_subject << std::endl;
return os;
}
main.cpp
#include "Person.h"
int main()
{
using std::cout;
using std::endl;
Person p1("zhangsan", 18);
Person p2(p1);
Person p3;
p3 = p1;
cout << "p1: \n" << p1 << endl;
cout << "p2: \n" << p2 << endl;
cout << "p3: \n" << p3 << endl;
Student stu1("lisi", 19, "man");
Student stu2(p1);
Student stu3(stu2);
Student stu4;
stu4 = stu1;
cout << "stu1: " << endl;
cout << stu1 << endl;
cout << "stu2: " << endl;
cout << stu2 << endl;
cout << "stu3: " << endl;
cout << stu3 << endl;
cout << "stu4: " << endl;
cout << stu4 << endl;
Teacher t1("wangwu", 17, "English");
Teacher t2(p2, "Math");
Teacher t3(t1);
Teacher t4;
t4 = t1;
cout << "t1: " << endl;
cout << t1 << endl;
cout << "t2: " << endl;
cout << t2 << endl;
cout << "t3: " << endl;
cout << t3 << endl;
cout << "t4: " << endl;
cout << t4 << endl;
return 0;
}
執行個體說明
-
類需要對Person
成員進行動态記憶體配置設定,是以需要自定義析構函數來釋放動态記憶體配置設定的記憶體,同時需要定義顯示複制構造函數和指派運算符來進行深拷貝。m_name
- 派生類
沒有使用動态記憶體配置設定,是以沒有定義顯示複制構造函數和指派運算符。對于繼承自基類的成員,Student
的預設拷貝構造函數使用Student
類的顯示拷貝構造函數來拷貝Person
對象的Student
部分。對于指派運算符來說也是如此,類的預設指派運算符将自動使用基類的指派運算符來對基類成員進行指派。Person
-
類需要對Teacher
成員進行動态記憶體配置設定,是以需要自定義析構函數來釋放動态記憶體配置設定的記憶體,同時需要定義顯示複制構造函數和指派運算符來進行深拷貝。m_subject
- 對于
類的指派運算符,需要顯示調用基類的指派運算符來對基類部分的成員進行指派,通過作用域解析運算符Teacher
來調用基類的指派運算符。::
// 派生類的複制構造函數
Teacher::Teacher(const Teacher &tea)
: Person(tea) // 調用基類的複制構造函數
{
m_subject = new char[strlen(tea.m_subject) + 1];
strcpy(m_subject, tea.m_subject);
}
// 派生類的指派運算符
Teacher& Teacher::operator=(const Teacher &tea)
{
if(this == &tea)
{
return *this;
}
Person::operator=(tea); // 調用基類的指派運算符,給基類部分的成員指派
delete [] m_subject;
m_subject = new char[strlen(tea.m_subject) + 1];
strcpy(m_subject, tea.m_subject);
return *this;
}
- 對于派生類的友元
,該函數隻能通路本類成員,如果需要使用基類的友元來通路基類的成員,可以通過強制類型轉換,将派生類轉換成基類,以便能夠通路派生類中的基類部分。operator<<()
std::ostream& operator<<(std::ostream& os, const Teacher& tea)
{
os << (const Person&)tea; // 強轉成基類,輸出基類部分的内容
os << "Subject: " << tea.m_subject << std::endl;
return os;
}
類設計總結
編譯器生成的成員函數
- 預設構造函數
- 預設構造函數要麼沒有參數,要麼所有的參數都有預設值。如果沒有定義任何構造函數,編譯器将定義預設構造函數,該預設構造函數将會自動調用基類的預設構造函數。
- 如果派生類構造函數的成員初始化清單中沒有顯示調用基類構造函數,則編譯器将使用基類的預設構造函數來構造派生類對象的基類部分。如果基類沒有預設構造函數,将導緻編譯錯誤。
- 如果定義了某種構造函數,編譯器将不會定義預設構造函數,這種情況下,需要自己提供預設構造函數。
- 是以最好提供一個顯示預設構造函數,将所有的類資料成員都初始化為合理的值。
- 複制構造函數
複制構造函數接受其所屬類的對象作為參數。例如Student類的複制構造函數原型如下:
Student(const Student &)
在以下情況下,将使用複制構造函數
- 将新對象初始化為一個同類對象;
- 按值将對象傳遞給函數;
- 函數按值傳回對象;
- 編譯器生成臨時對象。
如果程式沒有使用(顯示或隐式)複制構造函數,編譯器将提供原型,但不提供函數定義;否則, 程式将定義一個執行成員初始化的複制構造函數。
注意:如果使用new初始化成員指針則需要深拷貝,這時需要自己定義一個複制構造函數。
- 指派運算符
指派運算符用于處理同類對象之間的指派,差別于初始化。如果語句建立新的對象,則是初始化;如果語句修改已有對象的值,則是指派。
Student stu1;
Student stu2 = stu2; // 初始化,調用複制構造函數
Student stu3;
stu3 = stu1; // 指派,調用指派運算符
指派運算符原型如下:
Student & operator=(const Student &);
注意:如果使用new初始化成員指針則需要深拷貝,這時需要自己定義指派運算符。
其他類方法
- 構造函數
構造函數用于建立新的對象。
- 析構函數
析構函數用于做清理工作。一定要顯示定義析構函數來釋放類構造函數使用new配置設定的所有記憶體,并完成類對象所需的任何特殊的清理工作。對于基類,即使它不需要析構函數,也應提供一個虛析構函數。
- 轉換
使用一個參數就可以調用的構造函數定義了從參數類型到類類型的轉換。例如:
Student(const char *); // const char *轉換為Student類
Student(const Person &, int age = 1); // 從Person類轉換為Student類
将可轉換的類型傳遞給以類為參數的函數時,将調用轉換構造函數,例如:
Student stu1 = "zhangsan"; // 直接使用Student(const char *)構造出了Student對象
上述代碼中進行了隐式轉換,const char* 類型直接轉換為了Student類型
禁止隐式轉換使用explicit關鍵字,但仍可以進行顯示轉換
class Student
{
public:
explicit Student(const char *);
};
Student stu1 = "zhangsan"; // 報錯,禁止隐式轉換
Student stu1 = Student("zhangsan"); // 允許,允許顯示轉換
- 按值傳遞對象與傳遞引用
通常,編寫使用對象作為參數的函數時,應按引用而不是按值來傳遞對象。
原因一:為了提高效率。按值傳遞對象涉及到生成臨時拷貝,即調用複制構造函數,然後調用析構函數。調用這些函數需要時間,複制大型對象比傳遞引用花費的時間要多得多。
原因二:在繼承使用虛函數時,被定義為接受基類引用參數的函數可以接受派生類。
- 傳回對象和傳回引用
Student get_stu(const Student &); // 傳回對象
Student & get_stu(const Student &); // 傳回引用
- 傳回對象是傳回生成對象的臨時副本。
- 傳回引用可以節省時間和空間,因為傳回對象需要調用複制構造函數生成副本和析構函數删除副本。
如果函數傳回在函數中建立的臨時對象,則不要使用引用
Student Student::Sum(const Student &stu) const
{
return Student(m_scores + stu.m_scores); // 隻能傳回對象
}
如果函數傳回的是通過引用或指針傳遞給他的對象,則應按引用傳回對象。
const Student& Greater(const Student &stu1) const // 按引用傳回
{
if(m_scores > stu1.m_scores)
{
return *this;
}
else
{
return stu1;
}
}
- 使用const
const可以確定調用函數時不修改傳遞的參數。
Student::Student(const char *name) {...} // 不能修改name
成員函數使用const可以確定函數不修改調用它的對象。
void Student::Show() const {...} // 不能修改對象的參數
這裡的const表示const Student *this,this指向調用的對象。
什麼不能被繼承
- 構造函數不能被繼承。
- 因為建立派生類對象時,必須調用派生類的構造函數;
- 繼承意味着派生類對象可以使用基類的方法,然而,構造函數在完成其工作之前,對象并不存在,也就無法調用使用基類方法。
- 析構函數不能被繼承。因為,在釋放對象時,程式将首先調用派生類的析構函數,然後調用基類的析構函數。是以基類的析構函數應該被聲明為虛的。
- 指派運算符不能被繼承。因為,派生類繼承的方法的特征标與基類完全相同,但指派運算符的特征标随類而異,因為它包含一個類型為其所屬類的形參。
- 友元函數不能被繼承。因為友元函數并非類成員函數,是以不能被繼承。
基類和派生類互相指派的說明
- 将派生類賦給基類對象
Person p1; // 基類
Student stu1; // 派生類
p1 = stu1; // 派生類賦給基類
指派語句将被轉換成左邊的對象調用的一個方法
p1.operator=(stu1);
将調用基類的指派運算符
Person &operator=(const Person &p)
,但隻會處理基類成員,可以将派生類對象賦給基類對象,但隻涉及基類的成員。
- 将基類賦給派生類對象
Person p1; // 基類
Student stu1; // 派生類
stu1 = p1; // 基類賦給派生類
上述代碼将轉換為
stu1.operator(p1);
将調用派生類的指派運算符
Student & operator(const Student &)
,派生類不能自動引用基類對象,是以上述代碼會報錯。
解決辦法:
- 定義轉換構造函數
Student(const Person &);
轉換構造函數可以接受一個類型為基類的參數。也可以添加其他參數,但其他參數必須都有預設值
Teacher(const Person& p, const char *subject = "Chinese");
- 定義一個用于将基類賦給派生類的指派運算符:
Student & Student::operator=(const Person &) {...}
- 使用顯示強制類型轉換
Person p1; // 基類
Student stu1; // 派生類
stu1 = (Student)p1; // 基類賦給派生類
私有成員和保護成員
對于派生類而言,保護成員類似于公有成員;但對于外部而言,保護成員與私有成員類似。
派生類可以直接通路基類的保護成員,但隻能通過基類的成員函數來通路私有成員。
虛函數
如果希望派生類能夠重新定義方法,則應在基類中将方法定義為虛的。
class Person
{
...
public:
virtual void HShowAll() const {...} // 基類虛函數
...
};
class Student : public Person
{
...
public:
virtual void HShowAll() const {...} // 派生類重寫基類虛函數
...
};
void show(const Person& p)
{
p.HShowAll();
}
Person p;
Student stu1;
show(p); // 将調用Person類的HShowAll()
show(stu1); // 将調用Student類的HShowAll()
可見,虛函數可以讓對象正确調用相應的方法。
友元函數
由于友元函數并非類成員函數,是以不能繼承。如果希望派生類的友元函數能夠使用基類的友元函數,可以通過強制類型轉換,派生類引用或指針轉換為基類引用或指針,然後使用轉換後的指針或引用來調用基類的友元函數
std::ostream& operator<<(std::ostream& os, const Student &stu)
{
os << (const Person &)stu; // 強轉成基類,輸出基類部分的内容
os << "Sex: " << stu.m_sex << std::endl;
return os;
}
有關使用基類方法的說明
以公有方式派生的類的對象可以通過多種方式來使用基類的方法。
- 派生類對象可以使用繼承而來的公有或保護基類方法,如果派生類沒有重新定義該方法。
- 派生類的構造函數自動調用基類的預設構造函數,如果沒有在成員初始化清單中指定其他構造函數。
- 派生類構造函數顯示地調用成員初始化清單中指定的基類構造函數。
- 派生類方法可以使用作用域解析運算符
來調用公有的和受保護的基類方法。::
- 派生類的友元函數可以通過強制類型轉換,将派生類引用指針或指針轉換為基類引用或指針,然後使用該引用或指針來調用基類的友元函數。