类设计总结
实例一
本实例设计了三个类。第一个是基类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;
}
有关使用基类方法的说明
以公有方式派生的类的对象可以通过多种方式来使用基类的方法。
- 派生类对象可以使用继承而来的公有或保护基类方法,如果派生类没有重新定义该方法。
- 派生类的构造函数自动调用基类的默认构造函数,如果没有在成员初始化列表中指定其他构造函数。
- 派生类构造函数显示地调用成员初始化列表中指定的基类构造函数。
- 派生类方法可以使用作用域解析运算符
::
来调用公有的和受保护的基类方法。 - 派生类的友元函数可以通过强制类型转换,将派生类引用指针或指针转换为基类引用或指针,然后使用该引用或指针来调用基类的友元函数。