一个完整的类的实现的实例

参照 《CPP Primer Plus》中的 stock 类。

stock.h

  1. #ifndef STOCK_H
  2. #define STOCK_H
  3. #include <iostream>
  4. #include <string>
  5. class Stock
  6. {
  7. private:
  8. static
  9. std::string company;
  10. long shares;
  11. double share_val;
  12. double total_val;
  13. void set_tot() { total_val = shares * share_val; } //内联函数
  14. public:
  15. void acquire(const std::string& co, long n, double pr);
  16. void buy(long num, double price);
  17. void sell(long num, double price);
  18. void update(double price);
  19. void show() const; // const成员函数,用于不更改对象中的值的函数。这样const对象才能调用这个函数
  20. //构造和析构函数
  21. Stock();
  22. Stock(const std::string& co, long n, double pr);
  23. ~Stock();
  24. };
  25. #endif

stock.cpp

  1. #include <iostream>
  2. #include <string>
  3. #include "Stock.h"
  4. void Stock::acquire(const std::string& co, long n, double pr) {
  5. company = co;
  6. if (n < 0) {
  7. std::cout << "Number of shares can't be negative; "
  8. << company << " shares set to 0.\n";
  9. shares = 0;
  10. }
  11. else {
  12. shares = n;
  13. }
  14. share_val = pr;
  15. set_tot();
  16. }
  17. void Stock::buy(long num, double price) {
  18. shares += num;
  19. share_val = price;
  20. set_tot();
  21. }
  22. void Stock::sell(long num, double price) {
  23. shares = (shares - num >= 0) ? shares - num : 0;
  24. share_val = price;
  25. set_tot();
  26. }
  27. void Stock::update(double price) {
  28. share_val = price;
  29. set_tot();
  30. }
  31. void Stock::show() const {
  32. std::cout << "Company: " << company
  33. << " shares: " << shares << "\n"
  34. << " Share Price: $" << share_val
  35. << " Total Worth: $" << total_val << std::endl;
  36. }
  37. // 默认构造函数
  38. Stock::Stock()
  39. {
  40. company = "default";
  41. shares = share_val = total_val = 0;
  42. set_tot();
  43. }
  44. // 构造函数重载
  45. Stock::Stock(const std::string& co, long n=0, double pr=0):
  46. company(co), shares(n), share_val(pr)
  47. {
  48. set_tot();
  49. }
  50. // 默认析构函数
  51. Stock::~Stock(){}

main.cpp

  1. #include <string>
  2. #include "Stock.h"
  3. int main() {
  4. Stock Haymax("Haymax", 0, 0);
  5. Haymax.buy(10, 100);
  6. Haymax.show();
  7. return 0;
  8. }
  9. // 也可以使用 Stock Haymax {"Haymax", 0, 0}; 只要参数列表和一种构造函数的参数列表一样就行

类成员常量

在声明类的时候,如果只用 const 限定一个变量的话,这个变量只是对某一个具体的对象是常量不可改变的。这个变量只能在对象实例化的时候初始化。比如:

  1. class A{
  2. private:
  3. const int SIZE = 10;
  4. int my_array[SIZE]; // Error, SIZE对于这里不是一个const
  5. public:
  6. A();
  7. ~A();
  8. };

正确的写法应该是利用 static 关键字进行限定。此时该变量将存储在 静态存储区,进而不受对象实例化和对象的生命周期的影响

  1. class A{
  2. private:
  3. static const int SIZE = 10;
  4. int my_array[SIZE]; // Error, SIZE对于这里不是一个const
  5. };

运算符重载,友元函数

只需要把要重载的运算符放在 operator 关键字的后面就可以。

友元函数常用于二元运算符重载

函数原型

  1. // 成员函数中重载
  2. class Time{
  3. private:
  4. int hours=0;
  5. int minutes=0;
  6. public:
  7. // 成员函数,对象本身在操作数的左边。
  8. Time operator*{const double mult} const;
  9. // 非成员函数中重载二元运算符,因此无法操作私有成员,声明为友元
  10. friend Time operator*(const double mult, const Time & t);
  11. friend std::ostream & operator<<(std::ostream & os, const Time & t);
  12. }

函数定义

  1. Time Time::operator*(const double mult)const
  2. {
  3. Time result;
  4. long totalMinutes = hours * nult * 60 + minutes * 60;
  5. result.hours = totalminutes / 60;
  6. result.minutes = totalminutes % 60;
  7. return result;
  8. }
  9. Time operator*(const double mult, const Time & t)
  10. {
  11. return t * m; //这里就直接调用重载了
  12. }
  13. std::ostream & operator<<(std::ostream & os, const Time & t)
  14. {
  15. os << t.hours << " hours, "<< t.minutes << " minutes";
  16. return os;
  17. }

强制类型转换

一个类的强制类型转换,可以由只有一个参数的构造函数来进行,或者多个参数但是其余的给定了默认值。

利用关键字 explicit 可以屏蔽隐式的类型转换

类的原型

  1. class stoneWt
  2. {
  3. private:
  4. static const Lbs_per_stn = 14;
  5. int stone;
  6. double pds_left;
  7. double pounds;
  8. public:
  9. stoneWt(double lbs); // 强制类型转换
  10. stoneWt(int stn, double lbs);
  11. stoneWt();
  12. ~stoneWt();
  13. }

类的声明

  1. stoneWt::stoneWt(double lbs)
  2. {
  3. stone = int (lbs) / Lbs_per_stn;
  4. pds_left = int (lbs) % Lbs_per_stn;
  5. pounds = lbs;
  6. }

特殊成员函数

复制构造函数

用于将一个对象复制到新创建的对象中。复制构造函数是声明类的时候自动存在的,和默认构造函数类似。复制构造函数原型通常是

  1. Class_name(const Class_name &);

调用复制构造函数的情况

  1. // 假设存在一个类叫做MyClass
  2. MyClass object1;
  3. // 以下利用object1的方式都将调用复制构造函数
  4. MyClass object2(object1);
  5. MyClass Object2 = object1;
  6. MyClass object2 = MyClass object1;
  7. MyClass * object2 = new MyClass(object1)
  • !在函数按值传递的时候,也会调用复制构造函数
  • !函数返回对象(不是对象的引用)的时候,也会调用复制构造函数

总结来讲,就是需要创建对象的副本的时候就会调用复制构造函数。

默认复制构造函数的功能,是逐个复制对象的非静态成员。

!!需要格外注意的是,当这个类的成员变量中存在指针的时候,复制构造函数复制的是指针。这样的话,当一个对象的该成员变量被改变的时候,另一个也被改变了。而且当一个对象中的该变量被释放的时候,对于另一个对象,就很危险了。

解决方法:显示的编写复制构造函数,对指针变量进行深复制。

编写的时候有一点注意,private 访问限制是外部的。也就是说,因为复制构造函数仍然是类的成员,在声明复制构造函数的时候,传进来的变量是可以直接访问到 private 成员变量的。

赋值运算符重载

声明类的时候会自动定义默认的赋值运算符重载,其功能和默认复制构造函数有点像,都是逐个对非静态成员变量进行浅复制。

当类中有指针类型成员变量的时候,最好也能够显示的定义深复制,比如说

  1. ClassName & ClassName::operator=(const ClassName & object){
  2. // 防止自己赋值给自己
  3. if (this == & object){
  4. return *this;
  5. }
  6. delete [] old_ptr;
  7. old_ptr = new ptr_type;
  8. return *this;
  9. }

移动构造函数

这个是用于当对象是一个右值(临时值)的时候使用的。可以减少复制构造函数的使用,减少内存空间占用,优化代码。
移动构造函数要求,形参是该类的右值引用。因此当构造的时候使用右值,就会调用移动构造函数。

  1. // 拷贝构造函数
  2. MyString(const MyString& str) {
  3. CCtor ++;
  4. m_data = new char[ strlen(str.m_data) + 1 ];
  5. strcpy(m_data, str.m_data);
  6. }
  7. // 移动构造函数
  8. MyString(MyString&& str) noexcept
  9. :m_data(str.m_data) {
  10. MCtor ++;
  11. str.m_data = nullptr; //不再指向之前的资源了
  12. }

移动赋值函数

和移动构造函数类似,也是需要形参是本类的右值引用。

  1. // 拷贝赋值函数 =号重载
  2. MyString& operator=(const MyString& str){
  3. CAsgn ++;
  4. if (this == &str) // 避免自我赋值!!
  5. return *this;
  6. delete[] m_data;
  7. m_data = new char[ strlen(str.m_data) + 1 ];
  8. strcpy(m_data, str.m_data);
  9. return *this;
  10. }
  11. // 移动赋值函数 =号重载
  12. MyString& operator=(MyString&& str) noexcept{
  13. MAsgn ++;
  14. if (this == &str) // 避免自我赋值!!
  15. return *this;
  16. delete[] m_data;
  17. m_data = str.m_data;
  18. str.m_data = nullptr; //不再指向之前的资源了
  19. return *this;
  20. }

类继承

公有派生

  1. class NewClass NewClassName : public BaseClassName
  2. {
  3. ...
  4. }

使用共有派生的话,基类的公有成员将成为派生类的公有成员;积累的四有部分只能通过积累的共有和保护方法访问。

  • 派生类需要自己的构造函数
  • 派生类可以根据需要添加额外的数据成员和成员函数

派生类的构造函数需要在参数列表中显示地调用基类的构造函数,否则将直接调用基类的默认构造函数。在派生类的构造函数中,可以初始化派生类中新增的成员变量。

继承中的指针和引用

  • 派生类可以使用基类的非私有方法
  • 基类指针和引用可以不进行显式类型转换的情况下指向派生类对象。但是基类指针只能调用基类方法,无法调用指向的派生类的方法。

多态公有继承

两种实现方法:

  1. 在派生类中重新定义基类中的方法(不推荐,尽量不要重写基类中的非虚函数)
  2. 使用虚方法

在使用类中的方法时,如果直接利用对象调用,那么不存在任何问题。
但是存在另一种情况,基类的指针和引用都可以指向派生类。在用指针或者引用调用类中的方法的时候,编译器会根据指针/引用的类型决定调用哪种方法。
但是有的时候,我们更希望根据指针/引用所指向的对象的类型来决定使用哪一种方法。这个时候就需要使用 virtual 关键字限定的虚方法了。
在基类中定义为虚方法,那么在派生类中将自动被定义成虚方法。

注意

  • virtual 关键字只需要出现在原型中,不需要出现在函数定义中。
  • 析构函数一般都要声明称虚函数,除非不用做基类。

override关键字

当派生类继承的时候,如果基类中有一个虚函数,那么派生类将会自动继承这个虚方法的 接口 默认实现,除非进行显示的重写。
但是我们有的时候会忘记重写其中的某些虚函数,导致派生类中该函数使用的是 基类中的默认实现,与预期不符。
在派生类中,声明某个重写的虚函数的时候,最好加上 override关键字,表示这个方法是要重写基类中的虚函数。编译器会检查两个方面,一个是该函数是否是一个虚函数重载,另一个是它是否真的被实现了。
当然,这个关键字另一方面也是增加可读性。
继承的时候,看到virtual,还要重写,就记得加上就好了。

  1. class C: public testoverride
  2. {
  3. public:
  4. virtual void show() override;
  5. virtual void infor() override;
  6. virtual void vmendd() override;
  7. virtual void test(int x) override;
  8. virtual void splle() override;
  9. };

访问控制 protected

protected 控制的成员只有在类继承的过程中可以看出来。

在使用类的时候,protectedprivate 是类似的,都是不可访问的。在派生类中,则和 public 的表现类似是可以访问的。

抽象基类(ABC: Abstract Base Class)

抽象基类是包含纯虚函数的类,只能用作基类,而不能实例化。

纯虚函数

纯虚函数的定义要保证结尾处使用 =0,比如说

  1. virtual Type Func() const = 0;

包含纯虚函数的类是不能被实例化的。可以说,纯虚函数就是为继承,为重载而生的。

继承中的动态内存分配

众所周知,一旦类中出现了指针,需要进行动态内存分配,那么应当显示地编写 析构函数,复制构造函数,赋值运算符重载。在继承中的类如果出现了指针,也需要一些操作。

我们假设基类中存在指针,并且已经编写过了合适的上述三种函数,继承过程应当是这样的

派生类中没有新的指针

如果直接使用默认的三种函数,是 可以的

但是如果使用显示定义的 复制构造函数/赋值运算符重载,仍然需要现实的调用基类的相应函数。

派生类中有新的指针

这种情况下,一定需要重写这三种函数,那么就需要显示调用基类的复制构造函数/赋值运算符重载。

  1. NewClass::NewClass(const NewClass & object_)
  2. : BaseClass(object_)
  3. {
  4. ...
  5. }
  6. NewClass & NewClass::operator=(const NewClass & object)
  7. {
  8. ...
  9. BaseClass::operator=(object);
  10. ...
  11. }

派生类的友元

有的时候,派生类的友元需要访问基类的成员,但是因为友元函数不是成员函数,不能使用作用域解析运算来指定使用哪一个函数。这个时候,就需要进行强制类型转换,将派生类转换为基类之后,根据类型匹配,就可以调用基类的友元函数了。

常用于 << 运算符的重载。

私有继承

私有继承中,基类的共有和保护成员将成为派生类的私有成员。也就是说派生类对象不再能访问基类的公有成员了。

如果想要调用基类中的方法,可以使用积累名字加上作用域运算符来进行。

如果需要使用基类对象本身,需要将自己进行强制类型转换,变为一个基类对象。

保护继承

和私有继承类似,但是基类的私有和保护成员都将变成派生类的保护成员。

模板类

  1. template <typename Type1 typename Type2>
  2. class ClassName{
  3. ...
  4. }

定义函数的时候,需要在每个函数头前面用相同的模板声明开头。

  1. template <typename Type1 typename Type2>
  2. bool ClassName<Type1, Type2>::func(){
  3. ...
  4. }

注意! 由于模板类并不能被编译,不能放在单独的文件中实现。最直接的方法就是把函数的实现直接放到相应的头文件中。

使用的时候,必须显示地提供所需要的类型,才能够建立相应的具体化类和对象。但是函数模板是可以直接让函数自己判断。

模板类的使用和显示具体化

使用模板类,只需要显示提供相应的类型,编译器就会根据头文件中的定义自动的进行模板类的实例化

  1. ClassName<int, std::string> variableName

但是有的时候,对于一些特定的类型,不能使用模板类中通用的方法,需要对某一种类型进行一些方法上的定制化,这时候就需要显示具体化。

显式具体化可以将模板类中的模板变量全部明确,叫全特化,也可以将部分明确,叫偏特化

对于上面的模板类,如果想要显示实例化(全特化),其语法应该是:

  1. template<>
  2. class ClassName<real_type1, real_type2>{
  3. void func1(real_type1&, real_type2&);
  4. ...
  5. }
  6. // 实现其中函数的时候,不再需要template来进行约束,因为其中不再有模板类型
  7. void ClassName<real_type1, real_type2>::func1(real_type1&, real_type2&){
  8. ...
  9. }

下面是一个别人给的实例

  1. 作者:欧睿柠
  2. 链接:https://zhuanlan.zhihu.com/p/152211160
  3. 来源:知乎
  4. #include <iostream>
  5. using namespace std;
  6. template<class T1, class T2> class Point{
  7. public:
  8. Point(T1 x, T2 y): m_x(x), m_y(y){ }
  9. public:
  10. T1 getX() const{ return m_x; }
  11. void setX(T1 x){ m_x = x; }
  12. T2 getY() const{ return m_y; }
  13. void setY(T2 y){ m_y = y; }
  14. void display() const;
  15. private:
  16. T1 m_x;
  17. T2 m_y;
  18. };
  19. template<class T1, class T2>
  20. void Point<T1, T2>::display() const{
  21. cout<<"x="<<m_x<<", y="<<m_y<<endl;
  22. }
  23. template<> class Point<char*, char*>{
  24. public:
  25. Point(char *x, char *y): m_x(x), m_y(y){ }
  26. public:
  27. char *getX() const{ return m_x; }
  28. void setX(char *x){ m_x = x; }
  29. char *getY() const{ return m_y; }
  30. void setY(char *y){ m_y = y; }
  31. void display() const;
  32. private:
  33. char *m_x;
  34. char *m_y;
  35. };
  36. void Point<char*, char*>::display() const{
  37. cout<<"x="<<m_x<<" | y="<<m_y<<endl;
  38. }
  39. int main(){
  40. ( new Point<int, int>(10, 20) ) -> display();
  41. ( new Point<int, char*>(10, "E180") ) -> display();
  42. ( new Point<char*, char*>("E180", "N210") ) -> display();
  43. return 0;
  44. }

但是对于偏特化,有一点点区别。因为其中还有部分没有明确的可变的变量类型,对于上述两个例子,定义其中函数的时候,就应该是

  1. template<typename Type2>
  2. class ClassName<real_type1, Type2>{
  3. void func1(real_type1&, Type2&);
  4. ...
  5. }
  6. // 实现其中函数的时候,不再需要template来进行约束,因为其中不再有模板类型
  7. template<typename Type2>
  8. void ClassName<real_type1&, Type2>::func1(real_type1&, Type2&){
  9. ...
  10. }
  1. 作者:欧睿柠
  2. 链接:https://zhuanlan.zhihu.com/p/152211160
  3. 来源:知乎
  4. 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
  5. template<class T1, class T2>
  6. void Point<T1, T2>::display() const{
  7. cout<<"x="<<m_x<<", y="<<m_y<<endl;
  8. }
  9. template<typename T2>
  10. class Point<char*, T2>{
  11. public:
  12. Point(char *x, T2 y): m_x(x), m_y(y){ }
  13. public:
  14. char *getX() const{ return m_x; }
  15. void setX(char *x){ m_x = x; }
  16. T2 getY() const{ return m_y; }
  17. void setY(T2 y){ m_y = y; }
  18. void display() const;
  19. private:
  20. char *m_x;
  21. T2 m_y;
  22. };
  23. template<typename T2>
  24. void Point<char*, T2>::display() const{
  25. cout<<"x="<<m_x<<" | y="<<m_y<<endl;
  26. }

模板用作模板的参数

可以使用模板作为模板的参数,这样的话,可以使用一个泛型作为变量类型,就可以实现STL中的套娃定义

  1. template<template <typename T> typename Thing>
  2. class ClassName{
  3. ...
  4. }

模板类的友元函数

对于一般的函数,是不可以直接声明成模板类的友元函数的。因为没有模板函数这个对象,所以必须将相应的实例化作为函数的参数类型。

如果想要友元函数的参数类型是模板类,有两种方法:

  1. 约束模板友元函数,在外部声明函数的时候,声明为函数模板
  2. 非约束模板友元函数,类内声明友元函数的时候,声明为函数模板

约束模板友元函数

在类外声明为函数模板,让每一个类的具体化都有与友元匹配的具体化。就是说 int 类型的类具体化获得 int 类型的函数具体化

  1. template<typename T> void counts();
  2. template <typename T> void report(T &);
  3. template <typename TT>
  4. class myClass{
  5. friend void counts<TT>();
  6. friend void report<>(myClass<TT> &)
  7. }

非约束模板友元函数

非约束模板友元函数中,友元模板类型的参数可以和类模板的参数不同

  1. template <typename T>
  2. class myClass{
  3. ...
  4. template <typename C, typename D> friend void func1(C &, D &);
  5. };
  6. // 类外正常定义函数就可以
  7. template <typename C, typename D>
  8. void func1(C& c, D& d){
  9. ...
  10. }

为模板指定别名

  1. typedef std::array<double, 12> arrd;

也可以利用模板指定一系列别名

  1. template<typename T>
  2. using arrtype = std::array<T, 12>
  3. // 使用
  4. arrtype<int> my_int_array;

友元类

类似于电视机和遥控器的这种关系,显然利用继承的关系不太合适,但是遥控器确实应当有控制电视机的功能,也就是说和一般的其它东西能使用的电视机的功能应当不一样,所以就出现了友元类。

  • 友元的声明位置无所谓,可以在public, private, protected任何一个当中
  1. class TV{
  2. public:
  3. friend class RemoteControl;
  4. ...
  5. };

这个时候 RemoteControl 中的所有成员函数都可以调用 TV 中的所有私有成员。这个时候 RemoteControl 必须先了解 TV 类才可以。因此声明的顺序是

  1. class TV{
  2. ...
  3. }
  4. class RemoteControl{
  5. ...
  6. }

但是有的时候,即使一个友元类中,也只有很少的几个成员函数需要调用私有的成员。这个时候,可以只把这几个成员函数声明成友元,也就是 友元成员函数

  1. class Tv{
  2. friend void Remote::set_chan(Tv & t, int c);
  3. ...
  4. };

但是这里在声明的顺序上一定要注意,TV 类需要了解友元成员函数定义,但是友元成员函数中又用到了 TV,造成了一个循环依赖的困境。因此这个时候需要用到 前向声明 来解决这个问题

  1. class Tv; // forward declaration
  2. class RemoteControl { ... };
  3. class Tv { ... };

嵌套类

将类的声明放在另一个类中,称为嵌套类

嵌套类可以通过作用域运算符 :: 来进行访问,嵌套类的成员函数可以通过两次作用域运算符访问到。

嵌套类的位置,决定了嵌套类的访问权限,和一般的类成员别无二致