一个完整的类的实现的实例
参照 《CPP Primer Plus》中的 stock 类。
stock.h
#ifndef STOCK_H
#define STOCK_H
#include <iostream>
#include <string>
class Stock
{
private:
static
std::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对于这里不是一个const
public:
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 &);
调用复制构造函数的情况
// 假设存在一个类叫做MyClass
MyClass 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 declaration
class RemoteControl { ... };
class Tv { ... };
嵌套类
将类的声明放在另一个类中,称为嵌套类
嵌套类可以通过作用域运算符 ::
来进行访问,嵌套类的成员函数可以通过两次作用域运算符访问到。
嵌套类的位置,决定了嵌套类的访问权限,和一般的类成员别无二致