# 类 ## 一个示例 `string1.h`头文件 ```C++ #pragma once #ifndef STRING1_H_ #define STRING1_H_ #include using std::ostream; using std::istream; class String { private: char * str; int len; static int num_strings; static const int CINLIM = 80; public: //构造函数 String(const char * s); String(); //默认构造函数 String(const String &); //复制构造函数 ~String(); //析构函数 int length() const { return len; } static const int getCINLIM() { return CINLIM; } //重载操作符 String & operator=(const String &); String & operator=(const char *); char & operator[](int i); const char & operator[](int i) const; //重载操作符 友元函数 friend bool operator<(const String & st1, const String & st2); friend bool operator>(const String & st1, const String & st2); friend bool operator==(const String & st1, const String & st2); friend ostream & operator<<(ostream & os, const String & st); friend istream & operator>>(istream & is, String & st); //静态函数 static int HowMany(); }; #endif // !STRING1_H_ ``` `string1.cpp` 函数定义文件 ```C++ #include #include "string1.h" using std::cin; using std::cout; //静态类数据 int String::num_strings = 0; //静态类方法 int String::HowMany() { return num_strings; } //类成员函数 String::String(const char * s) { len = std::strlen(s); str = new char[len + 1]; std::strcpy(str, s); num_strings++; } String::String() { len = 4; str = new char[1]; str[0] = '\0'; num_strings++; } String::String(const String & st) { len = st.len; str = new char[len + 1]; std::strcpy(str, st.str); num_strings++; } String::~String() { --num_strings; delete[] str; } String & String::operator=(const String & st) { if (this == &st) return *this; delete[] str; len = st.len; str = new char[len + 1]; std::strcpy(str, st.str); return *this; } String & String::operator=(const char * s) { delete[] str; len = std::strlen(s); str = new char[len + 1]; std::strcpy(str, s); return *this; } char & String::operator[](int i) { return str[i]; } const char & String::operator[](int i) const { return str[i]; } //友元函数 bool operator<(const String & st1, const String & st2) { return (std::strcmp(st1.str, st2.str) < 0); } bool operator>(const String & st1, const String & st2) { return (std::strcmp(st1.str, st2.str) > 0); } bool operator==(const String & st1, const String & st2) { return (std::strcmp(st1.str, st2.str) == 0); } ostream & operator<<(ostream & os, const String & st) { os << st.str; return os; } istream & operator>>(istream & is, String & st) { char temp[80]; is.get(temp, String::getCINLIM()); if (is) { st = temp; } while (is && is.get() != '\n') { continue; } return is; } ``` `saying1.cpp` 使用文件 ```C++ #include #include "string1.h" const int ArSize = 10; const int MaxLen = 81; int main() { using std::cout; using std::cin; using std::endl; String name; cout << "What's your name?\n"; cin >> name; cout << "Your name is " << name << " Are you sure?"; String saying[ArSize]; char temp[MaxLen]; return 0; } ``` ## 构造函数 构造函数不同于其他类方法,它主要创建新的对象,其他类方法只是被现有对象调用。这也是构造函数不被继承的原因之一。 ### 1.不使用new的构造函数 正常写。 ### 2.使用new的构造函数 构造函数使用new时一般需要配合使用三个特殊方法:析构函数、复制构造函数、重载赋值运算符。 ```C++ class BaseDMA{ private: char * lable; int rating; public: BaseDMA(const char * l="null", int r=0); BaseDMA(const BaseDMA & rs); virtual ~BaseDMA(); BaseDMA & operator=(const BaseDMA & rs); }; ``` ## 析构函数 要注意定义显式的析构函数来释放构造函数使用new分配的堆内存空间,并完成类对象所需的任何特殊的清理工作。对于基类,即使它不需要析构函数,也应该提供一个虚析构函数。 ## 成员函数 普通的类方法都可以称为成员函数。 ## 运算符重载函数 将运算符重载为特定功能的函数。 ```C++ typeName operator+(typeName a); ``` ## 友元函数 友元函数不算成员函数,但是具有和成员函数一样的可以访问公有数据的能力。 ```C++ friend typeName operator<<(typeName a, typeName b); typeName operator<<(typeName a, TypeName b); //函数原型中写friend,而函数定义中不用写friend ``` ## 转换函数 ### 1.其他类型转换为类的类型 从其他类型转换为类的类型的对象,可使用一个参数的复制构造函数。 ```C++ Star(const char * a); //将char类型的C风格字符串转换为Star类型的对象。 ``` ### 2.类的类型转换为其他类型 要将类对象转换为其他类型,应定义特定的转换函数。转换函数可以是没有参数的类成员函数,也可以是返回类型被声明为目标类型的类成员函数。 ```C++ Star::Star double(){} Star::Star const char *(){} ``` ## 特殊成员函数 特殊成员函数是自动定义的,其中提供了一些成员函数。 - 默认构造函数,如果没有定义构造函数; - 默认析构函数,如果没有定义; - 复制构造函数,如果没有定义; - 赋值运算符,如果没有定义; - 地址运算符,如果没有定义。 ### 1.默认构造函数 默认构造函数要么没有参数,要么所有的参数都有默认值。如果没有定义任何构造函数,编译器将创建定义默认的构造函数,以便于创建对象。 ```C++ //默认构造函数,编译器将提供一个不接受任何参数也不执行任何操作的构造函数,因为创建对象时总是会调用构造函数。 Klunk::Klunk() {} ``` ### 2.复制构造函数 复制构造函数接受其所属类的对象引用作为参数。如果需要使用复制构造函数,但是类中又没有定义,那么一般编译器会自动生成一个复制构造函数,提供默认的原型,但不提供默认的定义。在下述情况下将使用复制构造函数。 - 将新对象初始化为一个同类对象; - 按值将对象传递给函数; - 函数按值返回对象; - 编译器生成临时对象。 ```C++ className(const className &) ``` ### 3.重载赋值运算符函数 默认的赋值运算符用于处理同类对象之间的赋值。不要将赋值与初始化混淆。如果语句创建新的对象,则使用初始化;如果语句修改已有对象的值,则是赋值。 ## 在构造函数中使用new注意事项 1. 如果在构造函数中使用`new` 来初始化指针成员,则应在析构函数中使用相应的delete释放内存。 2. new和delete必须相互兼容。`new` 对应于 `delete` , `new[]` 对应于 `delete[]` 。 3. 如果有多个构造函数,则必须以相同的方式使用new,要么都带中括号,要么都不带中括号,因为只有一个析构函数,所有构造函数必须于析构函数兼容。必要时可以在一个构造函数中使用new初始化指针,而在另一个构造函数中将指针初始化为空(0或者nullptr),delete可用于空指针。 4. 应定义一个复制构造函数,通过深复制将一个对象初始化为另一个对象。类似于下面。复制构造函数应分配足够的空间来存储复制的数据,并复制数据,而不仅仅是数据地址。 ```C++ String::String(const String & st){ num_strings++; len = st.len; str = new char[len+1]; std::strcpy(str, st.str); } ``` 5. 应当定义一个赋值运算符,通过深度复制将一个对象复制给另一个对象。类似于下面。 ```C++ String & String::operator=(const String & st){ if(this==&st) return *this; delete[] str; len = st.len; str = new char[len+1]; std::strcpy(str, st.str); return *this; } ``` ## 继承 ### 三种继承方式 - 公有继承(`is-a`) - 私有继承(`has-a`) - 保护继承(`has-a`) ### 两种继承形式 - 单继承 - 多继承 公有继承是常用的方式,它建立了一种is-a的关系,即派生类对象是一个基类对象。例如:Student是一个Person。再例如有一个Fruit类,保存水果的重量和热量,Banana是一个Fruit,因此从Fruit派生出Banana类。新的类将继承基类的所有数据成员。继承可以在基类的基础上添加属性和方法,但不能删除基类的属性。is-a关系通常是不可逆的。 私有继承也是has-a的关系。基类的公有成员和保护成员都将成为派生类的私有成员。这意味着基类方法将不会成为派生对象公有接口的一部分,但可以在派生类的成员中使用它们。 - 公有继承:基类的公有方法将会成为派生类的公有方法,is-a关系;获得实现,并且获得接口。基类中的公有成员和保护成员的访问权限在派生类中保持不变,基类的私有成员不可访问。 - 私有继承:基类的公有方法将会成为派生类的私有方法,has-a关系。获得实现,但不获得接口。基类中的公有成员和保护成员的访问权限在派生类中变为私有成员,基类的私有成员同样不可访问。 - 保护继承:基类的公有成员和保护成员将会成为派生类的保护成员,has-a关系。获得实现,但不获得接口。基类中的公有成员和保护成员的访问权限在派生类中变为保护成员,基类的私有成员同样不可访问。 保护继承是私有继承的变体,保护继承在列出基类时使用关键字 `protected` 。使用保护继承时,基类的公有成员和保护成员都将成为派生类的保护成员。 ==如果省略,编译器将默认为私有继承。== 单继承:一个派生类只有一个基类。多继承:同时有多个基类。单继承可以看成一个多继承的简单特例。多继承形式又可以叫做组合继承,组合继承是组合式的方式,它建立了一种has-a的关系。这种继承是一种包含和组合,继承实现并不继承接口。 ```C++ #include using namespace std; class A{ private: int x; //私有成员 protected: int y; //保护成员 public: void setx(int m) {x = m;} void sety(int n) {y = n;} void getx() {return x;} void gety() {return y;} } class B: public A{ public: int getsum(){ return getx() + y; } } int main(){ ... } ``` ```C++ #include using namespace std; class A { public: void SetA(int x) { this->x = x; } private: int x; }; class B { public: void SetB(int x) { this->y = y; } private: int y; }; class C { protected: void SetC(int x) { this->y = y; } private: int y; }; class ABC: public A, protected B { public: void SetABC(int x, int y, int z) { SetA(x); SetB(y); this->z = z; } private: int z; }; class ABC2: private ABC { public: void SetABC2(int x) { SetB(x); } }; int main(void) { ABC s1; s1.SetA(5); //s1.SetB(6); 非法!因为类B是保护继承,所以通过派生类的对象:不能直接访问从基类继承的任何成员 s1.SetABC(2, 5, 6); } ``` ### 成员初始化列表 派生类构造函数可以使用初始化列表机制将值传递给基类构造函数。头文件中不用写成员初始化列表,而定义的源文件中才写出成员初始化列表。 ```C++ Derived::Derived(type1 x, type2 y):Base(x, y){ ... } //其中Derived是派生类,Base是基类,Base(x, y)是初始化列表,使用基类构造函数进行初始化。 ``` ### 基类和派生类的指针和引用 基类对象的指针可以在不进行显式类型转换的情况下指向派生类对象;基类对象的引用可以在不进行显式类型转换的情况下引用派生类对象。但是不能使用基类指针或者引用调用派生类的方法。派生类对象的指针和引用不能指向基类对象。 通常C++不允许将一种类型的地址赋给另一种类型的指针,也不允许一种类型的引用指向另一种类型。除了基类指针和引用指向派生类的特例。 将派生类引用或指针转换为基类引用或指针称为向上强制转换(upcasting)。向上强制转换会使公有继承不需要进行显式类型转换。该规则是is-a关系的一部分。 相反的过程,将基类指针或引用转换为派生类的指针或引用称为向下强制转换(downcasting)。如果不使用显式类型转换,则向下强制转换是不允许的。原因是is-a关系通常是不可逆的。派生类可以新增数据成员,使用这些数据成员的类成员函数不能应用于基类。 ### 静态联编和动态联编 由于函数重载,编译器必须要根据参数列表来确定使用哪个函数。这就是联编(binding)。在编译过程中确定联编的称为静态联编(static binding),也可叫早期联编(early binding)。虚函数使得静态联编变得很困难,使用哪个函数不能再编译时确定。所以就有了在程序运行时选择正确的虚方法的动态联编,也叫做晚期联编(late binding)。 隐式向上强制转换使基类指针或引用可以指向基类对象或派生类对象,因此需要动态联编,C++使用虚成员函数来满足需求。编译器对虚方法使用动态联编。静态联编的效率更高,所以设置为默认的联编方式。 ## 多态 ### 多态公有继承 场景:希望同一个方法在派生类和基类中的行为不同,方法的行为取决于调用该方法的对象。这种复杂行为表示就是多态,顾名思义具有多种形态,即同一个方法的行为随上下文而异。有两种重要的机制可以实现多态公有继承。 - 在派生类中重新定义基类的方法。 - 使用虚方法。 ## 抽象基类 为什么要有抽象基类(Abstract Base Class,ABC)。场景:椭圆和圆,圆是一种特殊的椭圆。如果以椭圆为基类,圆从椭圆基类派生出,那么就会存在这样的问题:虽然圆是一种椭圆,但是这种派生是笨拙的。例如,圆只需要一个值(半径)就可以描述其大小形状,并不需要长半轴(a)和短半轴(b)。Circle构造函数可以通过将同一个值r赋给成员a和成员b来照顾这种情况,但是会导致信息冗余。angle参数和rotate方法对于圆来说没有意义。虽然可以用各种手段来修正各种不和谐问题,但是这些修正还不如直接重新定义一个新类Circle类更为简单。但是,圆和椭圆毕竟有太多共同点了,以至于在数学上定义为两类是不符合逻辑的。所以就要使用抽象基类。 从Ellipse和Circle类中抽象出它们的共性,将这些共性放到一个ABC中,然后从ABC派生出Ellipse和Circle。通过使用纯虚函数(pure virtual function)提供未实现的函数,纯虚函数声明的结尾处为=0。 ```C++ class BaseEllipse{ private: double x; double y; public: BaseEllipse(double x0=0, double y0=0): x(x0),y(y0){} virtual ~BaseEllipse(){} void Move(int nx, ny){x = nx; y = ny;} virtual double Area() const = 0; //纯虚函数 } ``` 当类声明中包含纯虚函数,则不能创建该类的对象,只能作为抽象基类被其他类继承。ABC为抽象基类,继承ABC的为具体类(concrete class),抽象基类不能创建对象,具体类可以创建对象。 ## 继承和动态内存分配 ### 派生类不使用new 基类的构造函数中使用了new,那么基类就要配置显式的析构函数、复制构造函数和重载赋值运算符,而派生类的构造函数中不使用new,那么全部保持默认即可。什么也不用多写。 ### 派生类使用new 基类的构造函数中使用了new,则基类就要配置显式的析构函数、复制构造函数和重载赋值运算符。相应的派生类的构造函数中也使用了new,那么就要配置相应的相应的显式的析构函数、复制构造函数和重载运算符。 ## 接口和实现 使用公有继承时,类可以继承接口,可能还有实现。获得接口是is-a关系的组成部分,而使用组合,类可以获得实现,但不能获得接口。不继承接口是has-a关系的组成部分。 ## 组合has-a示例 ```C++ //studentc.h #ifndef STUDENTC_H_ #define STUDENTC_H_ #include #include #include class Student{ private: typedef std::valarray ArrayDb; std::string name; ArrayDb scores; std::ostream & arr_out(std::ostream & os) const; public: Student(): name("Unknown"), scores() {} explicit Student(const std::string & s): name(s), scores() {} explicit Student(int n): name("Unknown"), socres(n) {} Student(const std::string & s, int n): name(s), scores(n) {} Student(const std::string & s, const ArrayDb & a): name(s), scores(a) {} Student(const char * str, const double * pd, int n): name(str), scores(pd, n) {} ~Student() {} double Average() const; const std::string & Name() const; double & operator[](int i); double operator[](int i) const; friend std::istream & operator>>(std::istream & is,Student & stu); friend std::istream & getlint(std::istream & is, Student & stu); friend std::ostream & operator<<(std::ostream & os, const Student & stu); }; #endif /* 关键字explicit 用一个参数调用的构造函数将从输入参数的其他类型转换为类的类型的隐式转换函数;这个通常会有问题。如 explicit Student(int n): name("Unknown"), scores(n) {} 这里的n表示数组元素的个数,而不是数组元素中的值。因此这个构造函数是一个从int到Student类型的转换函数,没有实际的意义,所以使用explicit关闭隐式转换。如果没有explicit则转换函数编译通过,下面的赋值会产生歧义。 Student stu9527("wang", 10) //姓名为wang和一个10元素的数组 stu9527 = 5; //重置姓名为"Unknown"和一个5元素的数组 使用explicit防止单参数构造函数的隐式转换。 */ ``` ## 私有继承has-a示例 ```C++ //studenti.h #ifndef STUDENTI_H_ #define STUDENTI_H_ #include #include #include class Student: private std::string, private std::valarray{ private: typedef std::valarray ArrayDb; std::ostream & arr_out(std::ostream & os) const; public: Student(): std::string("Unknown"), ArrayDb() {} explicit Student(const std::string & s): std::string(s), ArrayDb() {} explicit Student(int n): std::string("Unknown"), ArrayDb(n) {} Student(const std::string & s, int n): std::string(s), ArrayDb(n) {} Student(const std::string & s, ArrayDb & a): std::string(s), ArrayDb(a) {} Student(const char * str, const double * pd, int n): std::string(str), ArrayDb(pd, n) {} ~Student() {} double average() const; double & operator[](int i); double operator[](int i) const; const std::string & name() const; friend std::istream & operator>>(std::istream & is, Student & stu); friend std::istream & getline(std::istream & is, Student & stu); friend std::ostream & operator<<(std::ostream & os, const Student & stu); }; #endif ``` ## 使用包含还是使用私有继承 由于既可以使用包含又可以使用继承来建立has-a的关系,大多数人选择使用包含而不使用私有继承。包含易于理解,包含能够包括多个同类的子对象。继承会引起很多问题。 ## 类模板 类模板使得能够创建通用的类设计,其中类型(通常是成员类型)由类型参数表示。典型模板如下。 ```C++ template class Ic{ T v; ... public: Ic(const T & val): v(val) {} ... }; // T是类型参数,用作以后指定的实际类型的占位符,通常使用T或者Type // 也可以使用typename代替class ```