文章目录
类
private 私有成员
仍可被同类的其他对象函数直接调用
类中的内联函数
-
类声明中定义的函数等价于,在类声明中提供原型,然后在在类声明后提供内联函数定义,即,类声明中的成员函数定义是内联函数。
-
构造函数,无类型(void也没有)。若提供显式构造函数,则不要忘记写默认构造函数。
-
调用默认构造函数不要加括号,不然会误认为是函数定义。
Stock stocks();//Error!!! 这应该是返回值为Stock类的函数声明
const 成员函数
bool IsFull() const;
保证不在这个成员函数中修改该对象
- ***对于同一个函数,可提供其 const版本 与 非 const版本,以应对对象是否const的两种情况
char & operator[](int i);
const char operator[](int i) const;//返回const变量,不能用于左值
构造函数
无返回值(也不是void),函数名就是类名。
class Stack
{
private:
...
public:
...
Stack();
...
};
参数只有一个时,可以通过赋值的方式初始化给一个值(类型转换)
class time
{
private:
long int time;
int hours;
int min;
int seconds;
public:
time();
time(const long t);
...
};
...
time T=12;
//time T=time(12);
//time T(12);
当自己定义构造函数时,必须再定义默认构造函数,否则time T1;将错误
- 参数只有一个且为 指向类对象的常量引用,即 Class_name(const Class_name &);,此类构造函数又称为复制构造函数
- 只有接受一个参数且为的构造函数就是(类->其它类型 的)转换函数。
成员初始化列表
class time
{
private:
long int time;
int hours;
int min;
int seconds;
public:
time();
time(const long t);
time(const int h,const int m,const int s);
...
};
...
time(const int h,const int m,const int s):hours(h),min(m),seconds(s)
{}
- 这种初始化工作是在对象创建时完成的,此时还未执行构造函数括号中的任何代码。
- 这种格式只能用于构造函数。
- 非静态const 数据成员必须用这种方式初始化(C++11之前是)。
- 引用数据成员也必须用这种方式初始化。
- 数据成员初始化顺序与它们出现在类声明中的顺序相同,与初始化器中排序无关
- 还可用于常规内置类型初始化:
int games(162);
类内初始化(C++11)
class Classy
{
int mem1=10;
const int cmen2=12;
...
}
- 此方式与成员初始化列表效果一样
- 若再使用成员初始化列表重新初始化,则会覆盖类内初始化
析构函数
class Stack
{
private:
...
public:
...
~Stack();
...
};
类的类型转换和强制类型转换
类->其它类型 的自动转换
只有接受一个参数的构造函数就是(类->其它类型 的)转换函数。
假设类 fruit 存在转换函数—— fruit(double x);
则可以:
class fruit
{
...
public:
fruit(double x);
fruit(int n,double d=12.3);
...
}
...
fruit apple,pie;
apple = 12.4;//也叫 隐式转换,转换函数也叫自动类型转换函数
pie = 6;
等价于: apple - fruit(12.4);
即 用12.4作为参数构造出一个临时对象复制给apple
- 这种转换可在初始化,赋值,传参,返回 时自动进行
- 可以在无二义性时进行二次转换,比如 fruit→double→float
其它类型->类 的自动转换
转换函数:
-
必须是类方法
-
不能指定返回类型(但有返回值)
-
不能有参数
class fruit
{
private:
double weight;
…
public:
operator double(){return weight};
fruit(double w){weight = w;}
…
}
…
fruit apple(12.6);
double x = apple; //x=12.6
关闭自动类型转换——关键字 explicit
explicit fruit(int n,double d=12.3);
explicit operator double ();//c++11
但仍可强制类型转换
注意:
- 应谨慎使用隐式类型转换
- 过多的隐式转换极容易造成二义性造成错误
- 显式转换可用关键字 explicit 或直接用非转换函数
复制构造函数及其一些问题
- 参数只有一个且为 指向类对象的常量引用,即 Class_name(const Class_name &);
- 若没有自定义,则C++提供默认复制函数,成员的复制
- 默认构造函数可能会构成浅复制(复制指针)
- 深复制—— 对于指针,应该再创建一个新指针并开辟同样大小的空间然后复制原指针所指的内容到新空间中。
- 显式复构,即自定义的复构主要解决的就是 深浅复制问题(指针复制问题)
赋值运算符(=)的一些问题
- 数据是否出现交替,旧数据是否需要 delete? 以及重新 new新空间?
- 深浅复制问题
- 出现 自己 = 自己 的现象是会有哪些问题?是否特殊对待
- 重载函数应返回调用对象的引用,以进行连续赋值
- 赋值运算符是只能由类成员函数重载的运算符之一。
- 有时=会创建临时对象,导致构造…析构等一系列调用,这时可直接创建对应的赋值运算符重载来增加效率(见 例String)。
对象数组
对象数组必须有默认构造函数。
因为对象数组的初始化肯定经过默认初始化
类中的常量
一,const常量(只能用成员列表初始化)
首先,不能用 const 声明常量,因为类声明并不分配内存,而const常量的初始化必须在分配内存时进行。但是一旦对象进入构造函数,内存就已经分配,也就无法初始化,使用办法是在进入构造函数前就对其初始化——成员初始化列表。
class Stack
{
private:
const int MAX;
...
public:
Stack(int x);
};
...
Stack::Stack(int x):MAX(x)
{}
二,枚举
-
无名枚举
class Stack
{
private:
enum {MAX=10};
…
public:
…
}; -
作用域内枚举
两个枚举中的同名枚举量可能发生冲突,所以以下代码是错误的
enum egg{Small,Medium,Large};
enum apple{Small,Medium,Large};//!!!Error
以上是错误的
于是可用作用域内枚举
enum class egg{Small,Medium,Large};
enum class apple{Small,Medium,Large};
或者:
enum struct egg{Small,Medium,Large};
enum struct apple{Small,Medium,Large};
使用时必须用枚举名来限定枚举量
egg ch = egg::Large;
有些情况下,常规枚举将自动转换为 整型。但作用域内枚举将不会隐式转换为整型(可显式)
作用域内枚举底层类型,c++11默认为int,可自定义:
enum class:short pizza{Small,Medium,Large};
注意:
类声明中的枚举,在外界要用的时候还要有类的作用域解析符。
三,静态常量
class Stack
{
private:
static const int MAX=10;
...
public:
...
};
- 其内存与静态变量在一块。
- 无论创建了多少类对象,都只有一个静态成员副本。
- 注意:可以类声明中初始化整型或枚举型静态成员常量(const)
静态成员变量
-
无论创建了多少类对象,都只有一个静态成员副本。
-
注意:但不能在类声明中初始化静态成员变量。因为类声明不分配内存
-
对于静态类成员变量可以再类声明之外用单独的语句来进行初始化
-
初始化语句放在类的实现文件(.cpp)文件中
-
初始化语句指明类型并用了作用域解析符,但没有使用关键字 static
Stack.h
class Stack
{
private:
static int kk;
…
public:
…
};
…
Stack.cpp
int Stack::kk = 12;
静态成员函数
- 函数原型必须包含关键字 static 。定义不能包含关键字static
- 不能通过对象调用静态成员函数,也不能用 this指针 ,若在公有部分声明,则可用类名+作用域解析符 调用
- 不能与特定对象关联,因此只能使用静态数据成员
抽象数据类型
Stack.h
#ifndef STACKER_H_
#define STACKER_H_
typedef long int Item;
class Stack
{
private:
enum {MAX=10};
Item items[MAX];
int top;
public:
bool pop(Item &im);
bool push(const Item &im);
bool IsFull() const;
bool IsEmpty() const;
Stack();
};
#endif
Stack.cpp
#include"Stack.h"
#include<iostream>
Stack::Stack()
{
top=0;
}
bool Stack::pop(Item& im)
{
if(!IsEmpty()){
im=items[top-1];
top--;
return true;
}
else return false;
}
bool Stack::push(const Item& im)
{
if(!IsFull()){
items[top]=im;
top++;
return true;
}
else return false;
}
bool Stack::IsEmpty() const
{
/*if(top==MAX-1)
return true;
else return false;*/
return top==0;
}
bool Stack::IsFull() const
{
return top==MAX-1;
}
测试程序(main.cpp)
#include <iostream>
#include<cctype>
#include"Stack.h"
int main(int argc, char** argv) {
using namespace std;
Stack st;
enum col{MAX=10
};
char ch;
long po;
cout<<"Please enter A to add a purchase order.\n"
<<"P to process a PO, or Q to quit\n";
while(cin>>ch && toupper(ch)!='Q')
{
while(cin.get()!='\n');
if(!isalpha(ch))
{
cout<<'\a';
continue;
}
switch(ch)
{
case 'A':
case 'a':
cout<<"Enter a PO number to add: ";
cin>>po;
if(st.IsFull())
cout<<"stack already full\n";
else st.push(po);
break;
case 'p':
case 'P':
if(st.IsEmpty())
cout<<"stack already Empty\n";
else {
st.pop(po);
cout<<"PO # "<<po<<" poped\n";
}
break;
}
cout<<"Please enter A to add a purchase order.\n"
<<"P to process a PO, or Q to quit\n";
}
cout<<"Bye\n";
return 0;
}
运算符重载
Time.h
#ifndef TIME_H_
#define TIME_H_
class Time
{
private:
long int m_time;
int m_hours;
int m_minutes;
int m_seconds;
public:
Time();
Time(const long <ime);
~Time();
void ShowTime() const;
void SetTime(const long& ltime);
Time operator+(const Time& T) const;
Time Time::operator+(const long int& a) const;
};
#endif
time.cpp
#include<iostream>
#include"Time.h"
using std::cout;
using std::cin;
using std::endl;
Time::Time()
{
m_time=0;
m_hours=0;
m_minutes=0;
m_seconds=0;
}
Time::Time(const long<ime)
{
m_time=ltime;
m_hours=ltime/3600;
m_minutes=(ltime%3600)/60;
m_seconds=ltime%60;
}
void Time::SetTime(const long<ime)
{
m_time=ltime;
m_hours=ltime/3600;
m_minutes=(ltime%3600)/60;
m_seconds=ltime%60;
}
Time::~Time()
{
cout<<"The last time is :\n";
ShowTime();
cout<<" Bye~ \n";
}
void Time::ShowTime() const
{
cout<<m_hours<<" (h): "<<m_minutes<<" (m): "
<<m_seconds<<" (s):\n";
}
Time Time::operator+(const Time& T) const
{
return Time(m_time+T.m_time);
}
Time Time::operator+(const long int& a) const
{
return Time(m_time+a);
}
- 左边是调用对象,右边是被调用对象
- 至少有一个操作数是用户定义的类型
- 可以是重载成成员函数,或非成员函数(必要时用友元函数)
- 运算符的句法规则,优先级不变
- 以下运算符不能重载 p387
- “sizeof”运算符
- “.”成员运算符
- “.* ”成员指针运算符
- “::” 作用域解析运算符
- “? :” 三目条件运算符
- “typeid” 一个RTTI运算符
- “const_cast” 强制类型转换运算符
- “dynamic_cast” 强制类型转换运算符
- “reinterpret_cast” 强制类型转换运算符。
- “static_cast” 强制类型转换运算符
- 大多数运算符都可以通过成员或非成员函数进行重载,但下面 的运算符只能通过成员函数进行重载: p387
- “=” 赋值运算符
- “()” 函数调用运算符
- “[]” 下标运算符
- “->” 通过指针访问类成员的运算符
友元
友元有三种:
- 友元函数
- 友元类
- 友元成员函数
——赋予其访问类私有成员的权限
为什么使用友元函数?对于重载操作符时,有时两个参数不是同一类,这导致两个参数放左右两边效果不一样,因此可以将重载弄成非成员函数,这时为了方便访问其私有变量,可用友元函数
Time T1(12);
long int a = 24;
T1 = T1 + a;
创建友元之后才能:
T1 = a + T1;
对于非成员函数的重载运算符函数,比成员函数时多一个参数。
创建友元 friend
在要成为其友元的类声明中声明友元函数的原型,并在前面加关键字 friend
friend Time operator+(const long int ti,const Time& T);
注意:定义友元函数时就不用关键字 friend 了
常用友元:重载 <<
class Time
{
private:
...
public:
...
friend std::ostream& operator<<(const std::ostream& os,
const time tm);
};
例:Vector
Vector.h
#ifndef VECTOR_H_
#define VECTOR_H_
namespace VECTOR{
class Vector
{
public:
enum Mode{RECT,POL};
private:
double m_x;
double m_y;
double m_ang;
double m_mag;
Mode m_mode;
public:
//Vector
Vector();
Vector(const double n1,const double n2,const Mode mode=RECT);
~Vector();
//Set Vector
void SetX(const double x);
void SetY(const double y);
void ReSet(const double n1,const double n2,const Mode mode=RECT);
void SetAng(const double ang);
void SetMag(const double mag);
void SetMode(const Mode mode=RECT){m_mode=mode;};
//GetVector
double GetX()const {return m_x;}
double GetY()const {return m_y;}
double GetAng()const {return m_ang;}
double GetMag()const {return m_mag;}
Mode GetMode()const {return m_mode;}
//operator
Vector operator+(const Vector& v) const;
Vector operator-(const Vector& v) const;
Vector operator-() const;//重载减法运算符,两个参数为减,一个参数为负
//friend functions
friend Vector operator+(const double& d,const Vector& v);
friend Vector operator-(const double& d,const Vector& v);
friend Vector operator*(const double& d,const Vector& v);
friend std::ostream & operator<<(std::ostream &os,const Vector& v);
};
}
#endif
Vector.cpp
#include<iostream>
#include<cmath>
#include"Vector.h"
using std::cout;
using std::cin;
using std::endl;
using std::ostream;
using std::sqrt;
namespace VECTOR
{
//Vector
Vector::Vector()
{
m_x=0;
m_y=0;
m_ang=0;
m_mag=0;
m_mode=RECT;
}
Vector::Vector(const double n1,const double n2,const Mode mode)
{
m_mode=mode;
if(mode==RECT)
{
m_x=n1;
m_y=n2;
m_mag=sqrt(m_x*m_x+m_y*m_y);
if(m_y==0.0 && m_x==0.0)
m_ang=0;
else
m_ang=atan2(m_y,m_x);
}
else if(mode==POL)
{
m_ang=n1;
m_mag=n2;
m_x=m_mag*cos(m_ang);
m_y=m_mag*sin(m_ang);
}
else
{
cout<<"Incorrect 3rd argument to Vector() - - "
<<"Vector set to 0\n";
m_x=0;
m_y=0;
m_ang=0;
m_mag=0;
m_mode=RECT;
}
}
Vector::~Vector()
{
cout<<"Bye~"<<endl;
}
//Set Vector
void Vector::SetX(const double x)
{
m_x=x;
m_mag=sqrt(m_x*m_x+m_y*m_y);
if(m_y==0.0 && m_x==0.0)
m_ang=0;
else
m_ang=atan2(m_y,m_x);
}
void Vector::SetY(const double y)
{
m_y=y;
m_mag=sqrt(m_x*m_x+m_y*m_y);
if(m_y==0.0 && m_x==0.0)
m_ang=0;
else
m_ang=atan2(m_y,m_x);
}
void Vector::ReSet(const double n1,const double n2,const Mode mode)
{
m_mode=mode;
if(mode==RECT)
{
m_x=n1;
m_y=n2;
m_mag=sqrt(m_x*m_x+m_y*m_y);
if(m_y==0.0 && m_x==0.0)
m_ang=0;
else
m_ang=atan2(m_y,m_x);
}
else if(mode==POL)
{
m_ang=n1;
m_mag=n2;
m_x=m_mag*cos(m_ang);
m_y=m_mag*sin(m_ang);
}
else
{
cout<<"Incorrect 3rd argument to Vector() - - "
<<"Vector set to 0\n";
m_x=0;
m_y=0;
m_ang=0;
m_mag=0;
m_mode=RECT;
}
}
void Vector::SetAng(const double ang)
{
m_ang=ang;
m_x=m_mag*cos(m_ang);
m_y=m_mag*sin(m_ang);
}
void Vector::SetMag(const double mag)
{
m_mag=mag;
m_x=m_mag*cos(m_ang);
m_y=m_mag*sin(m_ang);
}
//operator
Vector Vector::operator+(const Vector& v) const
{
return Vector(v.m_x+m_x,v.m_y+m_y,RECT);
}
Vector Vector::operator-(const Vector& v) const
{
return Vector(m_x-v.m_x,m_y-v.m_y,RECT);
}
Vector Vector::operator-() const//重载减法运算符,两个参数为减,一个参数为负
{
return Vector(-m_x,-m_y,RECT);
}
//friend functions
Vector operator+(const double& d,const Vector& v)
{
return Vector(d+v.m_x,v.m_y);
}
Vector operator-(const double& d,const Vector& v)
{
return Vector(v.m_x-d,v.m_y);
}
Vector operator*(const double& d,const Vector& v)
{
return Vector(v.m_x*d,v.m_y*d);
}
ostream & operator<<(ostream &os,const Vector& v)
{
if(v.m_mode==Vector::RECT)
{
os<<v.m_x<<" + "<<v.m_y<<"i \n";
}
else
{
os<<v.m_mag<<"∠"<<v.m_ang<<endl;
}
}
}
类与动态内存
技巧:
- 即使只分配 1 个单位内存,也用 char * p = new char[1] ,这样释放内存时直接统一 delete[]p;
- 使用空指针nullptr,也兼容 delete[]p;
- 更换指针内容时,先delete原内存,再new新内存
- 注意深浅复制
例程(String1 P442)
//String1.h
#ifndef STRING1_H_
#define STRING1_H_
#include<iostream>
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 * st);
String();
String(const String & st);
~String();
int length() const {return len;}
//重载
String & operator=(const char *);
String & operator=(const String &);
char & operator[](int i);
const char operator[](int i) const;//返回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);
//以上三个友元的好处是,比起成员函数,可以将String对象与C字符串比较,因为const char*构造函数
friend ostream& operator<<(ostream &os,const String &st);
friend istream& operator>>(istream &is,String &st);
static int HowMany();
};
#endif
//String1.cpp
#include<cstring>
#include"string1.h"
using std::cout;
using std::cin;
//初始化静态成员
int String::num_strings=0;
int String::HowMany()
{
return num_strings;
}
//类方法定义
String::String()
{
len=0;//书上是4
str = new char[1];
str[0]='\0';
num_strings++;
}
String::String(const char * st)
{
len=std::strlen(st);
str = new char[len+1];
std::strcpy(str,st);
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.length();
str=new char[len+1];
std::strcpy(str,st.str);
return * this;
}
String &String::operator=(const char * st)
{
delete[]str;//记得更换内容时删除原空间
len=std::strlen(st);
str = new char[len+1];
std::strcpy(str,st);
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[String::CINLIM];
is.get(temp,String::CINLIM);
if(is)
st=temp;
while(is && is.get()!='\n')
continue;
//假定输入字符不多于String::CINLIM,并丢弃多余字符
//在if条件下,若由于某种原因(如到达文件尾或is.get(char*,int)读取是空行)导致输入失败,istream对象值被设为False
return is;
}
更高效——返回对象的引用
注意:参数是 const引用,而返回参数对象引用时必须也是const
再谈 定位 new 运算符
-
delete 不能与定位运算符配合使用 !
-
对于 定位new 分配内存的类对象,只能显式调用析构函数。
class fruit
{
…
};
…
char * buffer = new char[40];
…
fruit * fp = new (buffer) fruit;
…
delete fp;//错误!
delete[] buffer;//可以,但是不会调用 fruit 的析构函数
类中嵌套结构,类声明
- 类声明中声明的结构,类或枚举被称为是嵌套在类中,其作用域为整个类,并不会创建数据对象,只是指定了可在类中声明的数据类型。
- private 中声明的则只能再类中使用
- public 中声明的可在类外部通过作用域解析符声明该类型。
is-a关系 (公有继承)
公有继承 is-a
class fruit
{
private:
int weight;
int prince;
public:
fruit();
fruit(const int w,const int p);
bool SetWeight(int w);
int GetWeight();
...
};
...
class apple:public fruit
{
...
};
- fruit 是 apple 的公有基类
- 派生类需要自己的构造函数,可拓展新的数据成员和方法
- 公有继承——构造,析构
- 创建派生类对象时,先创建基类对象
- 需先使用列表初始化完成对基类构造:
apple::apple(const int w,const int p):fruit(w,p); - 若不调用基类构造函数则会调用默认构造函数
- 析构是,先析构派生类,再析构基类
2.使用派生类
关于继承成员的属性变化:
- 公有基类的 public成员 ->派生类的公有成员
- 公有基类的 private成员 ->派生类的私私有成员(只能用过基类的公有和保护方法访问)
- 公有基类的 protected成员 ->可被派生类调用,对外部如同私有成员(就像派生类的私有成员)
基类方法的调用
-
派生类中,对于需要重定义的方法,使用作用域解析运算符来调用基类方法(不需重定义的方法就不需要作用域解析符)。否则:
void BrassPlus::ViewAcct() const
{…ViewAcct();//Error!造成无限递归 }
protected成员
- 对基类而言,就是私有成员
- 对派生类而言,可被派生类调用,对外部如同私有成员(就像派生类的私有成员)
注意:
- 类的数据成员最好还是私有属性,保护属性可以用于类的内部方法。
关于基类指针和引用(多态):
向上强制转换:
- 基类指针可以在不进行显式类型转换的情况下指向派生类
- 基类引用可以在不进行显式类型转换的情况下引用派生类对象
注意:
- 基类指针/引用只能调用基类方法
- 若用派生类对基类进行赋值/复制/传参,或是将派生类对象作为基类传递参数时,只是传递了派生类的基类部分。而使用基类指针/引用则可以传递派生类:
void fr(Brass & rb){rb.ViewAcct();}
void fp(Brass * pb){pb->ViewAcct();}
void fv(Brass b){b.ViewAcct();}
…
Brass b(…);
BrassPlus bp(…);
fr(b);//uses Brass::ViewAcct()
fr(bp);//uses BrassPlus::ViewAcct()
fp(b);//uses Brass::ViewAcct()
fp(bp);//uses BrassPlus::ViewAcct()
fv(b);//uses Brass::ViewAcct()
fv(bp);//uses Brass::ViewAcct()
公有继承的关系:is-a 关系
在派生类中重写继承而来的部分函数(多态)
注意:重定义的方法与基类方法参数特征标不同时,并不会产生两个重载版本,而是派生类中新定义的方法隐藏了基类版本。
=> 两条经验:
- 派生类中重新定义的方法应该保证参数特征标一样,允许返回类型不一样
- 如果基类声明重载了,则应在派生类中重新定义所有的基类版本。否则未被重新定义的重载将会被隐藏,新定义可直接调用基类版本(通过作用域解析符)
一:直接重写
- 基类对象调用则用基类的方法,派生类对象调用则用派生类重定义的方法
- 对于基类指针指向派生类对象或基类引用引用派生类对象的情况:调用指针/引用类型 (基类)的方法
二:虚函数virtual
创建方式:
- 在基类中声明的函数前 加上关键字 virtual,它在派生类中将自动成为虚方法。
- 在派生类对应虚方法前加上关键字virtual,以作为标记。
特性:
- 对于基类指针指向派生类对象或基类引用引用派生类对象的情况:调用所指向/引用对应类型(派生类)的方法
- 基类声明虚析构函数,可保证释放对象时以正确的顺序调用析构:否则对指向派生类对象的基类指针进行delete则只调用基类的析构函数。
了解:由于虚函数,对于同名函数的调用需要在运行时具体确定,被称为——动态联编
(一般是静态联编)
使用:
- 即使不需要显式析构函数,也要将基类析构函数定义为虚函数
- 构造函数不需要用虚函数。
- 友元不能是虚函数,因为虚函数必须是类成员
- 在派生链中,将使用最新的虚函数版本
抽象基类ABC(纯虚函数)
- C++通过纯虚函数提供未实现的函数,纯虚函数声明的结尾处为=0。
- 包含至少一个纯虚函数的类不能创建对象,只能继承派生其他类。
- 纯虚函数可以不提供定义。
- ABC 要求派生类覆盖其纯虚函数。
继承和动态内存分配
情况一:基类使用new 而派生类不用new
使用动态内存分配的基类来说,需要注意特殊方法:析构函数,复制构造函数和重载赋值函数
若派生类新增部分不需要new 则这三个特殊方法也不需要显式定义
- 派生类析构函数执行完后自动调用基类析构函数
- 派生类的默认复制构造函数使用基类复制构造函数复制基类部分
- 派生类的默认复制运算符自动调用基类的赋值运算符
派生类与基类的这种关系也适用于 本身是对象的类成员。
情况二:派生类使用new
class baseDMA
{
private:
char * label;
int rating;
public:
baseDMA(const char * l="null",int r=0);
baseDMA(const baseDMA & rs);
virtual ~baseDMA();
baseDMA & operator=(const baseDMA & rs);
...
};
...
class hasMDA:public baseDMA
{
private:
char * style;
public:
hasDMA(const char * l="null",int r=0);
hasDMA(const hasDMA & rs);
~ hasDMA();
hasDMA & operator=(const hasDMA & rs);
}
需要注意特殊方法:析构函数,复制构造函数和重载赋值函数 都必须显式定义
析构函数:
只需负责派生类新增部分的处理,派生类的析构执行完后自动调用基类的析构函数
baseDMA::~baseDMA()
{
delete[]label;
}
hasDMA::~hasDMA()
{
delete[]style;
}
复制构造函数:
baseDMA::baseDMA(const baseDMA & rs)
{
label = new char[std::strlen(rs.label)+1];
std::strcpy(label,rs.label);
rating=rs.rating;
}
hasDMA::hasDMA(const hasDMA & hs):baseDMA(hs)
{
style = new char[std::strlen(hs.style)+1];
std::strcpy(style,hs.style);
}
赋值运算符重载函数:
派生类的赋值运算符重载函数属于同名函数的重新定义,因而会时基类的对应函数隐藏,因而需要显式调用基类的该函数,完成全部复制
//基类的赋值运算符重载函数:
baseDMA & baseDMA::operator=(const baseDMA & rs)
{
if(this == &rs)
return *this;
delete[]label;
label = new char[std::strlen(rs.label)+1];
std::strcpy(label,rs.label);
rating = rs.rating;
return *this;
}
//派生类的赋值运算符重载函数:
hasDMA & hasDMA::operator=(const hasDMA & hs)
{
if(this == &hs)
return *this;
baseDMA::operator=(hs);
delete[]style;
label = new char[std::strlen(rs.style)+1];
std::strcpy(style,rs.style);
return *this;
}
访问基类的友元
将派生类对象强制类型转换可得到基类对象
class baseDMA
{
private:
char * label;
int rating;
public:
friend std::ostream & operator<<(std::ostream &os,const baseDMA* rs);
...
};
...
class hasMDA:public baseDMA
{
private:
char * style;
public:
friend std::ostream & operator<<(std::ostream &os,const hasDMA* hs);
...
}
对于friend std::ostream & operator<<(std::ostream &os,const hasDMA* hs); 此函数并不是基类的友元,所以无法访问基类的 label 和 rating 等私有成员,所以可以通过显式类型转换调用基类的友元函数:
friend std::ostream & operator<<(std::ostream &os,const hasDMA* hs)
{
os<<(const baseDMA &)hs;
os<<"Style: "<<hs.style<<endl;
return os;
}
has-a关系(包含对象成员,保护和私有继承)
has-a 关系的特点:不继承接口
is-a 关系的特点:继承接口,不一定继承实现(纯虚函数)
模板类——valarray
—— 使用动态内存分配
-
声明对象
valarray q_values;
valarray weights;
2.初始化
-
默认:长度为0的空数组
-
一个参数的(n)构造:长度为n的数组
-
两个参数(a,n)的构造:长度为n,且各位被初始化为a 的数组
-
两个参数(p,n)的构造(第一个参数是数组,第二个整型),长度为n,且初始化为数组p的前n个
-
初始化列表(C++11):
double gpa[5]={3.1,4.2,5.6,12.4,16.8};
valarray v1;
valarray v2(8);//an array of 8 int elements
valarray v2(10,8);//an array of 8 int elements,each set to 10
valarray v4(gpa,4);//an array of 4 double elements,set to {3.1,4.2,5.6,12.4}valarray v5 = {1,2,3,4,5};//C++11
- 该类的一些方法
- operator
- size()
- sum()
- max()
- min()
- (待添加)
包含对象成员
class Student
{
private:
string name;
valarray<double> scores;
...
};
初始化:
Student::Student(const char* str,const double * pd,int n)
:name(str),scores(pd,n){}
- 若不以初始化成员列表,则调用默认构造函数初始化 name 和 scores 对象。
- 注意初始化顺序,是对象成员在类声明中声明的顺序,而不是初始化成员列表中的顺序。
私有继承
继承成员的属性变化
- 基类的公有成员变成私有成员(不继承接口)
私有继承的声明
注意:不写关键字private,默认继承方式也是私有继承
class Student:private std::string,private std::valarray<double>
{
...
}
- 包含提供两个被显式命名的成员对象,而私有继承提供了两个无名称的子对象成员
私有继承的构造(初始化)
Student::Student(const char* str,const double * pd,int n)
:std::string(str),std::scores(pd,n){}
基类方法的调用
必须使用 类名+作用域解析符
访问基类对象
-
将派生类对象强制类型转换可得到基类对象
//相当于得到上面包含对象版本的name对象成员
const string& Student::Name const
{
return (const string &)*this;
}
访问基类的友元
ostrean & operator<<(ostream &os,const Student & stu)
{
os<<"Scores for"<<(const String & )stu<<":\n";
...
}
- 原因在于:1.私有继承中,不进行显示类型转换就不能将指向派生类的引用或指针赋给基类引用或指针;2.有多个基类,无法确定转换为哪个基类
- 公有继承也要这样做的原因则是:会导致无限递归
私有继承,还是 包含对象成员?
两者区别在于:
-
- 私有继承是 提供无名称的子对象成员,而包含对象成员提供被显式命名的对象成员
-
- 初始化不同:一个用对象名,一个用类名
-
- 使用基类方法方式:一个用对象名,一个用类名+作用域解析符
两者优势:
- 对于包含对象成员:
- 可包含多个对象
- 易于理解
- 相比于私有继承,不易出错
- 对于私有继承:
- 特性更多
- 可以访问基类的保护成员
- 派生类可重新定义虚函数(注意:因为是私有继承,重新定义的虚方法都是私有成员)
保护继承
- 基类的公有成员和保护成员都变成派生类的保护成员(不继承接口)
- 与私有继承区别在于:私有继承到第三代时,第三代类不能使用基类接口,而保护继承可以
类继承总结
类继承属性转换总结
特征 公有继承 保护继承 私有继承
基类的公有成员变成 派生类的公有成员 派生类的保护成员 派生类的私有成员
基类的保护成员变成 派生类的保护成员 派生类的保护成员 派生类的私有成员
基类的私有成员变成 只能同基类接口访问(私私有) 只能同基类接口访问(私私有) 只能同基类接口访问(私私有)
能否隐式向上转换 能 只在派生类中能 不能
类继承访问权限的更改
在私有/保护继承中,若想让基类的接口也成为了派生类的接口,则可以:
方法一:再定义一个调用基类方法的接口
double Student::sum() const
{
return std::valarray<double>::sum();
}
方法二:使用 using
用 using 指出派生类可以使用特点的基类成员,即使是私有派生
class Student:private std::string,private std::valarray<double>
{
...
public:
using std::valarray<double>::sum;
...
}
则 sum 就像 Student 的公有方法一样调用即可
Student ada("Alice",{90.2,96.3,94.8},3);
cout<<ada.sum();
注意:
- using 声明只需成员名,没有函数特征标,圆括号,返回类型等
- 将会使该成员的两个版本 const 和 非 const 都可用
- using 声明只适用于继承,不适用于包含成员对象
方法三(老式,即将被摒弃) 重新声明基类方法
class Student:private std::string,private std::valarray<double>
{
...
public:
std::valarray<double>::operator[];
...
}
像不使用using 关键字的 using声明。
多重继承MI
有多个直接基类的类,每个类都要写明继承方式
class Worker
{
string name;
int ID;
public:
...
};
class Singer:public Worker
{
int voice;
...
};
class Waiter:public Worker
{
int panache;//译:神气,气质
};
class SingerWaiter:public Singer,public Waiter
{
...
};
第一代基类有几个?
SingerWaiter ed;
Worker * pr = &ed;//!!!Error
//SingerWaiter对象中包含两个第一代基类Worker,Worker指针无法确定指向哪一个
//解决方法:强制类型转化
worker * pr = (Waiter *)&ed;
事实上,并不需要多个第一代基类 worker 存在于 SingerWaiter 中,所以可以采用——虚基类
虚基类
虚基类 使得从多个类(这多个类的基类相同)派生出的对象只继承一个基类对象
创建虚基类
在类声明(继承部分)使用关键字 virtual 。
class Singer:virtual public Worker{...};
class Waiter:public virtual Worker{...};
//virtual 与 public 的顺序无影响
class SingerWaiter:public Singer,public Waiter{...};
虚基类特性
使 SingerWaiter 继承的 Waiter 和 Singer 类共享同一个 Worker基类
=> SingerWaiter 只包含一个 Worker 子对象
虚基类相关的构造函数
SingerWaiter::SingerWaiter(const Worker &wk,int p=0,int v=Singer::Other):Waiter(wk,p),Singer(wk,v){}//!!!Error
虚基类不允许通过中间类的方式调用构造函数,因为有两种途径调用基类的构造,所以上述代码中,wk 并不会传递给两个子对象。
解决方式是:显式调用虚基类的构造函数:
SingerWaiter::SingerWaiter(const Worker &wk,int p=0,int v=Singer::Other):Worker(wk),Waiter(wk,p),Singer(wk,v){}
对于虚基类上述方法正确,对于非虚基类则是非法的。
使用哪个方法?
若想通过派生类调用继承而来的同名函数(派生类并未重新定义该函数),可能导致二义性。
SingerWaiter newhire("Elis Hwaks",2005,6,Other);
newhire.show();//!!!ambigious
//解决方式:用类名+作用域解析符:
newhire.Singer::show();
模块化设计+私有/保护辅助方法,见P560
虚基类与非虚基类混合使用:
则最终派生类中,包含一个表示所有的虚途径的基类子对象和分别表示各条非虚途径的多个基类子对象0
虚二义性(成员名的优先)
- 非虚基类从不同基类继承而来的同名成员,直接调用将会导致二义性,但虚基类不一定,存在虚二义性(成员名的优先)
- 派生类中的名称优先于直接或间接祖先类中的相同名称
- 虚二义性规则与访问规则无关,即使优先的那个成员是私有的无法访问的,也不能直接调用不优先的那个公有的。
- 详见 P566,567
类模板
定义类模板
template<class Type>//template <typename Type>也行
class Stack
{
private:
enum {MAX=10};
Type items{MAX};
int top;
public:
Stack();
bool isempty() const;
bool isfull() const;
bool push(const Type &item);
bool pop(Type & item);
};
template <class Type>
Stack<Type>::Stack()
{
top=0;
}
template <class Type>
bool Stack<Type>::isempty()
{
return top==0;
}
...
- 模板类定义前加 template Type就表示泛型
- 模板类的成员函数定义前也要加该关键语句,且类名改为 类名
- 关键语句中的 class 可改为 typename
- 模板类只是编译指令,所以应该类模板的所有信息都在 头文件 中。
使用模板类
-
仅在程序中包含模板并不能生成模板类,而必须请求实例化
Stack kernels;
Stack coloels;
注意:类模板使用时,必须显式提供类型。而模板函数,编译器则可根据参数类型自行判断。
深入讨论模板类
指针栈:
类型为指针的一种栈,无论将指针视为纯指针,数组名还是new得到一段空间 都是不行的(P572,P573)。正确使用指针栈的方法是:让调用程序提供一个指针数组,不同的指针指向不同的内存,这样吧指针放在栈中才是有意义的。
注意:创建不同指针是调用程序的职责而不是栈的职责,栈只负责管理指针,不负责创建指针。
例子stcktp1 (P574,575,576)(待添加)
数组模板示例和非类型参数
允许指定数组大小的简单数组模板:方法一是:在类中使用动态数组和构造函数参数来提供元素数组;另一方法是:使用模板参数来提供常规数组的大小(c++11新增的模板array就是这样),下面示例将演示
///arrattp.h
#ifndef ARRAYTP_H_
#define ARRAYTP_H_
#include<iostream>
#include<csdlib>
template<class T,int n>
class ArrayTp
{
private:
T ar[n];
public:
ArrayTp(){};
explicit ArrayTp(const T & v);
virtual T& operator[](int i);
virtual T operator[](int i) const;
};
template <class T,int n>
ArrayTp<T,n>::ArrayTp(const T&v)
{
for(int i=0;i<n;i++)
ar[i]=v;
}
template <class T,int n>
T & Array<T,n>::operator[](int i)
{
if(i<0 || i>=n)
{
std::cout<<"Error in array limits:"<<i
<<"is out of range\n";
std::exit(EXIT_FAILURE);
}
return ai[i];
}
template <class T,int n>
T Array<T,n>::operator[](int i) const
{
if(i<0 || i>=n)
{
std::cout<<"Error in array limits:"<<i
<<"is out of range\n";
std::exit(EXIT_FAILURE);
}
return ai[i];
}
#endif
使用:
ArrayTp<double,12> eggweights;
与动态内存分配的方法相比,其缺点是:
- 参数不同,就会生成不同的几分类声明
- ArrayTp<T,n>类对象 一旦声明,其数组大小就已经确定,无法改变,也无法将大小不同的两个数组互相赋值。
模板多功能性
1.可用作基类,组件类,其他模板类的类型参数
2.递归用法:
ArrayTp< ArrayTp<int,5>,10 > twodee;
类似于 int two[10][5]
注意:> >(中间有空格) 与 >> 区分开来 (C++11就不用了)
3.使用多个模板参数
4.默认类型模板参数
template <class T1,class T2 = int> class Topo {...};
虽然可以给类模板提供默认类型,但不能给模板函数提供。而非类型参数则两者都能提供默认值。
模板的具体化
隐式实例化
编译器只在需要类对象时创建对应的类定义,并生成对应的对象
注:只定义对象指针不new不算“对象创建”
显式实例化
关键字 template :
//!!!该声明必须位于模板定义所在的名称空间中
template class ArrayTp<string,100>;
显式具体化
对特殊类型进行特殊具体化
template <> class ArraySort<const char*>{...};
//早期:
class ArraySort<const char*>{...};
部分具体化
-
部分限制模板的通用性
//general template
template <class T1,class T2> class Pair{…};
//specialization with T2 set to int
template class Pair<T1,int>{…};
//specialization with T1,T2 set to int,相当于显式具体化
template <> class Pair<int,int>{…};
若有多个模板类可供选择,则优先具体化程度最高的
-
指针提供特殊版本来部分具体化现有模板
template
class Feeb{…};template<class T*>
class Feeb{…}; -
部分具体化特性能提供各种限制
//general template
template <class T1,class T2,class T3> class Tiro{…};
//specialization with T3 set to T2
template <class T1,class T2> class Tiro<T1,T2,T2>{…};
//specialization with T1,T2 set to T1*
template class Tiro<T1,T1*,T1*>{…};
成员模板
template <typename T>
class bete
{
private:
template <typename V>//模板类
class hold
{
...
}
hold<T> q;
hold<int> g;
public:
template<typename U>
U blab(U u,T t){...}//模板函数
...
}
可以在类声明中只声明模板类和模板函数,而在类外定义:
template <typename T>
class beta
{
private:
template <typename V>//模板类
class hold;
hold<T> q;
hold<int> g;
public:
template<typename U>
U blab(U u,T t);//模板函数
...
}
template <typename T>
template <typename V>
class beta<T>::hold
{
...
};
template <typename T>
template <typename U>
U beta<T>::blab(U u,T t)
{
...
}
将模板用作参数
模板还可以包含本身就是模板的参数
template <template <typename T> class Thing>
class Crab
{...};
//若有:
template <typename T>
class King{...};
//则可以:
Crab<King> legs;
模板类和友元
模板的友元分三类:
- 非模板友元
- 约束(bound)模板友元,即友元的类型取决于类被实例化时的类型(1-1)
- 非约束(unbound)模板友元,即友元的所有具体化都是类的每一个具体化的友元(n-n)
模板类的非模板友元函数
template<class T>
class HasFriend
{
public:
friend void counts();
...
};
注意,不能 friend void report(HasFriend &); 因为并不存在 HasFriend 这个类,而只有特定的具体化,要提供模板类参数,必须指明具体化,可以这样:
template<class T>
class HasFriend
{
public:
friend void report(HasFriend<T> &);
...
};
...
HasFriend<short> hf1;
HasFriend<int> hf1;
//注意:report本身并不是模板函数,而是只能使用一个模板作为参数,这意味着必须要为使用的友元显式具体化:
void report(HasFriend<short> &);
void report(HasFriend<int > &);
模板类的约束模板友元函数
三步:
-
1.再类定义前声明每个模板函数:
template void counts();
template void report(T &); -
2.在模板类中再次将模板声明为友元:
template
class HasFriend
{
public:
friend void counts();
friend void report<>(HasFriend &);
…
};
//!!!在声明时模板具体化,对于report,<>可以空,因为可根据参数判断出模板类型参数为 HasFriend
//当然,也可以:friend void report<HasFriend >(HasFriend &);
//一种具体化类只对应一种友元函数 -
3.为友元函数提供模板定义
模板类的非约束模板友元函数
每个友元的具体化都能访问所有的具体化类。
template<class T>
class ManyFriend
{
public:
template<typename C,typename D>friend void show2(C &,D &);
...
};
template<typename C,typename D>friend void show2(C &,D &)
{
...
}
...
Manyfriend<int> hfi;
Manyfriend<double> hfdb;
show2(hfi,hfdb);
模板别名
typedef 语句
typedef std::array<double,12> arrd;
arrd gallons;//gallons的类型是:std::array<double,12>
using= 语句(C++11)
template<typename T>
using arrtype = std::array<T,12>;
...//arrtype<T> 就表示 std::array<T,12>
arrtype<double> gallons;//gallons的类型是:std::array<double,12>
arrtype<int> days;//days的类型是:std::array<int,12>
C++11 允许 语法 using= 用于非模板,非模板中该语法与常规 typedef 一样
typedef const char* pc1;
//<=>
using pc2 = const char*;
typedef const int * (*pa1)[10];//我认为这是个指向一个指针数组的指针
//<=>
using pa2 = const int * (*)[10];
C++新增:可变参数模板(待补充,18章)
接受可变数量的参数的模板类或模板函数