一个完整的类的实现的实例
参照 《CPP Primer Plus》中的 stock 类。
stock.h
#ifndef STOCK_H#define STOCK_H#include <iostream>#include <string>class Stock{private:staticstd::string company;long shares;double share_val;double total_val;void set_tot() { total_val = shares * share_val; } //内联函数public:void acquire(const std::string& co, long n, double pr);void buy(long num, double price);void sell(long num, double price);void update(double price);void show() const; // const成员函数,用于不更改对象中的值的函数。这样const对象才能调用这个函数//构造和析构函数Stock();Stock(const std::string& co, long n, double pr);~Stock();};#endif
stock.cpp
#include <iostream>#include <string>#include "Stock.h"void Stock::acquire(const std::string& co, long n, double pr) {company = co;if (n < 0) {std::cout << "Number of shares can't be negative; "<< company << " shares set to 0.\n";shares = 0;}else {shares = n;}share_val = pr;set_tot();}void Stock::buy(long num, double price) {shares += num;share_val = price;set_tot();}void Stock::sell(long num, double price) {shares = (shares - num >= 0) ? shares - num : 0;share_val = price;set_tot();}void Stock::update(double price) {share_val = price;set_tot();}void Stock::show() const {std::cout << "Company: " << company<< " shares: " << shares << "\n"<< " Share Price: $" << share_val<< " Total Worth: $" << total_val << std::endl;}// 默认构造函数Stock::Stock(){company = "default";shares = share_val = total_val = 0;set_tot();}// 构造函数重载Stock::Stock(const std::string& co, long n=0, double pr=0):company(co), shares(n), share_val(pr){set_tot();}// 默认析构函数Stock::~Stock(){}
main.cpp
#include <string>#include "Stock.h"int main() {Stock Haymax("Haymax", 0, 0);Haymax.buy(10, 100);Haymax.show();return 0;}// 也可以使用 Stock Haymax {"Haymax", 0, 0}; 只要参数列表和一种构造函数的参数列表一样就行
类成员常量
在声明类的时候,如果只用 const 限定一个变量的话,这个变量只是对某一个具体的对象是常量不可改变的。这个变量只能在对象实例化的时候初始化。比如:
class A{private:const int SIZE = 10;int my_array[SIZE]; // Error, SIZE对于这里不是一个constpublic:A();~A();};
正确的写法应该是利用 static 关键字进行限定。此时该变量将存储在 静态存储区,进而不受对象实例化和对象的生命周期的影响
class A{private:static const int SIZE = 10;int my_array[SIZE]; // Error, SIZE对于这里不是一个const};
运算符重载,友元函数
只需要把要重载的运算符放在 operator 关键字的后面就可以。
友元函数常用于二元运算符重载
函数原型
// 成员函数中重载class Time{private:int hours=0;int minutes=0;public:// 成员函数,对象本身在操作数的左边。Time operator*{const double mult} const;// 非成员函数中重载二元运算符,因此无法操作私有成员,声明为友元friend Time operator*(const double mult, const Time & t);friend std::ostream & operator<<(std::ostream & os, const Time & t);}
函数定义
Time Time::operator*(const double mult)const{Time result;long totalMinutes = hours * nult * 60 + minutes * 60;result.hours = totalminutes / 60;result.minutes = totalminutes % 60;return result;}Time operator*(const double mult, const Time & t){return t * m; //这里就直接调用重载了}std::ostream & operator<<(std::ostream & os, const Time & t){os << t.hours << " hours, "<< t.minutes << " minutes";return os;}
强制类型转换
一个类的强制类型转换,可以由只有一个参数的构造函数来进行,或者多个参数但是其余的给定了默认值。
利用关键字 explicit 可以屏蔽隐式的类型转换
类的原型
class stoneWt{private:static const Lbs_per_stn = 14;int stone;double pds_left;double pounds;public:stoneWt(double lbs); // 强制类型转换stoneWt(int stn, double lbs);stoneWt();~stoneWt();}
类的声明
stoneWt::stoneWt(double lbs){stone = int (lbs) / Lbs_per_stn;pds_left = int (lbs) % Lbs_per_stn;pounds = lbs;}
特殊成员函数
复制构造函数
用于将一个对象复制到新创建的对象中。复制构造函数是声明类的时候自动存在的,和默认构造函数类似。复制构造函数原型通常是
Class_name(const Class_name &);
调用复制构造函数的情况
// 假设存在一个类叫做MyClassMyClass object1;// 以下利用object1的方式都将调用复制构造函数MyClass object2(object1);MyClass Object2 = object1;MyClass object2 = MyClass object1;MyClass * object2 = new MyClass(object1)
- !在函数按值传递的时候,也会调用复制构造函数
- !函数返回对象(不是对象的引用)的时候,也会调用复制构造函数
总结来讲,就是需要创建对象的副本的时候就会调用复制构造函数。
默认复制构造函数的功能,是逐个复制对象的非静态成员。
!!需要格外注意的是,当这个类的成员变量中存在指针的时候,复制构造函数复制的是指针。这样的话,当一个对象的该成员变量被改变的时候,另一个也被改变了。而且当一个对象中的该变量被释放的时候,对于另一个对象,就很危险了。
解决方法:显示的编写复制构造函数,对指针变量进行深复制。
编写的时候有一点注意,private 访问限制是外部的。也就是说,因为复制构造函数仍然是类的成员,在声明复制构造函数的时候,传进来的变量是可以直接访问到 private 成员变量的。
赋值运算符重载
声明类的时候会自动定义默认的赋值运算符重载,其功能和默认复制构造函数有点像,都是逐个对非静态成员变量进行浅复制。
当类中有指针类型成员变量的时候,最好也能够显示的定义深复制,比如说
ClassName & ClassName::operator=(const ClassName & object){// 防止自己赋值给自己if (this == & object){return *this;}delete [] old_ptr;old_ptr = new ptr_type;return *this;}
移动构造函数
这个是用于当对象是一个右值(临时值)的时候使用的。可以减少复制构造函数的使用,减少内存空间占用,优化代码。
移动构造函数要求,形参是该类的右值引用。因此当构造的时候使用右值,就会调用移动构造函数。
// 拷贝构造函数MyString(const MyString& str) {CCtor ++;m_data = new char[ strlen(str.m_data) + 1 ];strcpy(m_data, str.m_data);}// 移动构造函数MyString(MyString&& str) noexcept:m_data(str.m_data) {MCtor ++;str.m_data = nullptr; //不再指向之前的资源了}
移动赋值函数
和移动构造函数类似,也是需要形参是本类的右值引用。
// 拷贝赋值函数 =号重载MyString& operator=(const MyString& str){CAsgn ++;if (this == &str) // 避免自我赋值!!return *this;delete[] m_data;m_data = new char[ strlen(str.m_data) + 1 ];strcpy(m_data, str.m_data);return *this;}// 移动赋值函数 =号重载MyString& operator=(MyString&& str) noexcept{MAsgn ++;if (this == &str) // 避免自我赋值!!return *this;delete[] m_data;m_data = str.m_data;str.m_data = nullptr; //不再指向之前的资源了return *this;}
类继承
公有派生
class NewClass NewClassName : public BaseClassName{...}
使用共有派生的话,基类的公有成员将成为派生类的公有成员;积累的四有部分只能通过积累的共有和保护方法访问。
- 派生类需要自己的构造函数
- 派生类可以根据需要添加额外的数据成员和成员函数
派生类的构造函数需要在参数列表中显示地调用基类的构造函数,否则将直接调用基类的默认构造函数。在派生类的构造函数中,可以初始化派生类中新增的成员变量。
继承中的指针和引用
- 派生类可以使用基类的非私有方法
- 基类指针和引用可以不进行显式类型转换的情况下指向派生类对象。但是基类指针只能调用基类方法,无法调用指向的派生类的方法。
多态公有继承
两种实现方法:
- 在派生类中重新定义基类中的方法(不推荐,尽量不要重写基类中的非虚函数)
- 使用虚方法
在使用类中的方法时,如果直接利用对象调用,那么不存在任何问题。
但是存在另一种情况,基类的指针和引用都可以指向派生类。在用指针或者引用调用类中的方法的时候,编译器会根据指针/引用的类型决定调用哪种方法。
但是有的时候,我们更希望根据指针/引用所指向的对象的类型来决定使用哪一种方法。这个时候就需要使用 virtual 关键字限定的虚方法了。
在基类中定义为虚方法,那么在派生类中将自动被定义成虚方法。
注意:
virtual关键字只需要出现在原型中,不需要出现在函数定义中。- 析构函数一般都要声明称虚函数,除非不用做基类。
override关键字
当派生类继承的时候,如果基类中有一个虚函数,那么派生类将会自动继承这个虚方法的 接口 和 默认实现,除非进行显示的重写。
但是我们有的时候会忘记重写其中的某些虚函数,导致派生类中该函数使用的是 基类中的默认实现,与预期不符。
在派生类中,声明某个重写的虚函数的时候,最好加上 override关键字,表示这个方法是要重写基类中的虚函数。编译器会检查两个方面,一个是该函数是否是一个虚函数重载,另一个是它是否真的被实现了。
当然,这个关键字另一方面也是增加可读性。
继承的时候,看到virtual,还要重写,就记得加上就好了。
class C: public testoverride{public:virtual void show() override;virtual void infor() override;virtual void vmendd() override;virtual void test(int x) override;virtual void splle() override;};
访问控制 protected
protected 控制的成员只有在类继承的过程中可以看出来。
在使用类的时候,protected 和 private 是类似的,都是不可访问的。在派生类中,则和 public 的表现类似是可以访问的。
抽象基类(ABC: Abstract Base Class)
抽象基类是包含纯虚函数的类,只能用作基类,而不能实例化。
纯虚函数
纯虚函数的定义要保证结尾处使用 =0,比如说
virtual Type Func() const = 0;
包含纯虚函数的类是不能被实例化的。可以说,纯虚函数就是为继承,为重载而生的。
继承中的动态内存分配
众所周知,一旦类中出现了指针,需要进行动态内存分配,那么应当显示地编写 析构函数,复制构造函数,赋值运算符重载。在继承中的类如果出现了指针,也需要一些操作。
我们假设基类中存在指针,并且已经编写过了合适的上述三种函数,继承过程应当是这样的
派生类中没有新的指针
如果直接使用默认的三种函数,是 可以的。
但是如果使用显示定义的 复制构造函数/赋值运算符重载,仍然需要现实的调用基类的相应函数。
派生类中有新的指针
这种情况下,一定需要重写这三种函数,那么就需要显示调用基类的复制构造函数/赋值运算符重载。
NewClass::NewClass(const NewClass & object_): BaseClass(object_){...}NewClass & NewClass::operator=(const NewClass & object){...BaseClass::operator=(object);...}
派生类的友元
有的时候,派生类的友元需要访问基类的成员,但是因为友元函数不是成员函数,不能使用作用域解析运算来指定使用哪一个函数。这个时候,就需要进行强制类型转换,将派生类转换为基类之后,根据类型匹配,就可以调用基类的友元函数了。
常用于 << 运算符的重载。
私有继承
私有继承中,基类的共有和保护成员将成为派生类的私有成员。也就是说派生类对象不再能访问基类的公有成员了。
如果想要调用基类中的方法,可以使用积累名字加上作用域运算符来进行。
如果需要使用基类对象本身,需要将自己进行强制类型转换,变为一个基类对象。
保护继承
和私有继承类似,但是基类的私有和保护成员都将变成派生类的保护成员。
模板类
template <typename Type1, typename Type2>class ClassName{...}
定义函数的时候,需要在每个函数头前面用相同的模板声明开头。
template <typename Type1, typename Type2>bool ClassName<Type1, Type2>::func(){...}
注意! 由于模板类并不能被编译,不能放在单独的文件中实现。最直接的方法就是把函数的实现直接放到相应的头文件中。
使用的时候,必须显示地提供所需要的类型,才能够建立相应的具体化类和对象。但是函数模板是可以直接让函数自己判断。
模板类的使用和显示具体化
使用模板类,只需要显示提供相应的类型,编译器就会根据头文件中的定义自动的进行模板类的实例化
ClassName<int, std::string> variableName
但是有的时候,对于一些特定的类型,不能使用模板类中通用的方法,需要对某一种类型进行一些方法上的定制化,这时候就需要显示具体化。
显式具体化可以将模板类中的模板变量全部明确,叫全特化,也可以将部分明确,叫偏特化
对于上面的模板类,如果想要显示实例化(全特化),其语法应该是:
template<>class ClassName<real_type1, real_type2>{void func1(real_type1&, real_type2&);...}// 实现其中函数的时候,不再需要template来进行约束,因为其中不再有模板类型void ClassName<real_type1, real_type2>::func1(real_type1&, real_type2&){...}
下面是一个别人给的实例
作者:欧睿柠链接:https://zhuanlan.zhihu.com/p/152211160来源:知乎#include <iostream>using namespace std;template<class T1, class T2> class Point{public:Point(T1 x, T2 y): m_x(x), m_y(y){ }public:T1 getX() const{ return m_x; }void setX(T1 x){ m_x = x; }T2 getY() const{ return m_y; }void setY(T2 y){ m_y = y; }void display() const;private:T1 m_x;T2 m_y;};template<class T1, class T2>void Point<T1, T2>::display() const{cout<<"x="<<m_x<<", y="<<m_y<<endl;}template<> class Point<char*, char*>{public:Point(char *x, char *y): m_x(x), m_y(y){ }public:char *getX() const{ return m_x; }void setX(char *x){ m_x = x; }char *getY() const{ return m_y; }void setY(char *y){ m_y = y; }void display() const;private:char *m_x;char *m_y;};void Point<char*, char*>::display() const{cout<<"x="<<m_x<<" | y="<<m_y<<endl;}int main(){( new Point<int, int>(10, 20) ) -> display();( new Point<int, char*>(10, "E180") ) -> display();( new Point<char*, char*>("E180", "N210") ) -> display();return 0;}
但是对于偏特化,有一点点区别。因为其中还有部分没有明确的可变的变量类型,对于上述两个例子,定义其中函数的时候,就应该是
template<typename Type2>class ClassName<real_type1, Type2>{void func1(real_type1&, Type2&);...}// 实现其中函数的时候,不再需要template来进行约束,因为其中不再有模板类型template<typename Type2>void ClassName<real_type1&, Type2>::func1(real_type1&, Type2&){...}
作者:欧睿柠链接:https://zhuanlan.zhihu.com/p/152211160来源:知乎著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。template<class T1, class T2>void Point<T1, T2>::display() const{cout<<"x="<<m_x<<", y="<<m_y<<endl;}template<typename T2>class Point<char*, T2>{public:Point(char *x, T2 y): m_x(x), m_y(y){ }public:char *getX() const{ return m_x; }void setX(char *x){ m_x = x; }T2 getY() const{ return m_y; }void setY(T2 y){ m_y = y; }void display() const;private:char *m_x;T2 m_y;};template<typename T2>void Point<char*, T2>::display() const{cout<<"x="<<m_x<<" | y="<<m_y<<endl;}
模板用作模板的参数
可以使用模板作为模板的参数,这样的话,可以使用一个泛型作为变量类型,就可以实现STL中的套娃定义
template<template <typename T> typename Thing>class ClassName{...}
模板类的友元函数
对于一般的函数,是不可以直接声明成模板类的友元函数的。因为没有模板函数这个对象,所以必须将相应的实例化作为函数的参数类型。
如果想要友元函数的参数类型是模板类,有两种方法:
- 约束模板友元函数,在外部声明函数的时候,声明为函数模板
- 非约束模板友元函数,类内声明友元函数的时候,声明为函数模板
约束模板友元函数
在类外声明为函数模板,让每一个类的具体化都有与友元匹配的具体化。就是说 int 类型的类具体化获得 int 类型的函数具体化
template<typename T> void counts();template <typename T> void report(T &);template <typename TT>class myClass{friend void counts<TT>();friend void report<>(myClass<TT> &)}
非约束模板友元函数
非约束模板友元函数中,友元模板类型的参数可以和类模板的参数不同
template <typename T>class myClass{...template <typename C, typename D> friend void func1(C &, D &);};// 类外正常定义函数就可以template <typename C, typename D>void func1(C& c, D& d){...}
为模板指定别名
typedef std::array<double, 12> arrd;
也可以利用模板指定一系列别名
template<typename T>using arrtype = std::array<T, 12>// 使用arrtype<int> my_int_array;
友元类
类似于电视机和遥控器的这种关系,显然利用继承的关系不太合适,但是遥控器确实应当有控制电视机的功能,也就是说和一般的其它东西能使用的电视机的功能应当不一样,所以就出现了友元类。
- 友元的声明位置无所谓,可以在public, private, protected任何一个当中
class TV{public:friend class RemoteControl;...};
这个时候 RemoteControl 中的所有成员函数都可以调用 TV 中的所有私有成员。这个时候 RemoteControl 必须先了解 TV 类才可以。因此声明的顺序是
class TV{...}class RemoteControl{...}
但是有的时候,即使一个友元类中,也只有很少的几个成员函数需要调用私有的成员。这个时候,可以只把这几个成员函数声明成友元,也就是 友元成员函数
class Tv{friend void Remote::set_chan(Tv & t, int c);...};
但是这里在声明的顺序上一定要注意,TV 类需要了解友元成员函数定义,但是友元成员函数中又用到了 TV,造成了一个循环依赖的困境。因此这个时候需要用到 前向声明 来解决这个问题
class Tv; // forward declarationclass RemoteControl { ... };class Tv { ... };
嵌套类
将类的声明放在另一个类中,称为嵌套类
嵌套类可以通过作用域运算符 :: 来进行访问,嵌套类的成员函数可以通过两次作用域运算符访问到。
嵌套类的位置,决定了嵌套类的访问权限,和一般的类成员别无二致
