c++面向对象程序设计
学习笔记,仅供参考,图片来源于网络
文章目录
- c++面向对象程序设计
- 一、头文件与类的声明
- 二、构造函数
- 三、参数传递
- 四、操作符重载与临时对象
- 五、复习complex类实现过程
- 六、实现对有指针成员的类string
- 七、三大函数
- 八、堆heap,栈stack与内存管理
- 九、复习string类实现过程
- 十、补充
- 十一、组合与继承
- 十二、虚函数与多态
- 十三、转换函数(conversion function)
- 十四、类指针类(pointer-like classes)
- 十五、仿函数(function-like classes)
- 十六、成员模板(member template)
- 十七、模板特化(specialization)
- 十八、模块模板参数(tempalte template parameter)
- 十九、可变参数模板(variadic templates-since)
- 二十、自动类型推导(auto)
- 二十一、更简洁的for(ranged-base for)
- 二十二、引用(reference)
- 二十三、虚指针(vptr)和虚表(vtbl)
- 二十四、this
- 二十五、Dynamic Binding动态绑定
- 二十六、const
- 二十七、重载Operator new,operator delete
一、头文件与类的声明
1、头文件声明
#include <iostream.h> // 引入标准库
#include "complex.h" // 引入自己定义
//延伸文件名(extension file name) 不一定是.h 或.cpp,也可能是.hpp 或其他或甚至無延伸名。
2、c++输出格式
#include <iostream.h> // 引入输出库
using namespace std;
int main()
{
int i = 7;
cout << “i=“ << i << endl;
return 0;
}
3、头文件写法
以complex.h
为例,写头文件,用于防卫式声明
#ifndef __COMPLEX__ //如果未声明__COMPLEX__,则往下执行define语句,到endif结束,__COMPLEX__名称可以自己改变
#define __COMPLEX__
.....//包含三部分:0:前卫声明、1:类的声明、2:类的定义
#endif
对于第1部分:类的声明,以如下为例:
class complex //任何类需要一个class head类头
{ //往下开始写class body
public:
complex (double r = 0, double i = 0): re (r), im (i){ }// body内直接定义
complex& operator += (const complex&); // 仅在此声明
double real () const { return re; } // body内直接定义
double imag () const { return im; } // body内直接定义
private:
double re, im;
friend complex& __doapl (complex*, const complex&);
};
注:有些函數在class body内直接定义,另一些在body 之外定义
对于第2部分:类的定义,包含许多函数,如下:
inline double imag(const complex& x){
return x.imag ();
}
inline double real(const complex& x){
return x.real ();
}
...
最后实例化
{
complex c1(2,1);
complex c2;
...
}
为了避免重复,引入template (模板),类的声明改为:
template<typename T>
class complex
{
public:
complex (T r = 0, T i = 0): re (r), im (i){ }
complex& operator += (const complex&);
T real () const { return re; }
T imag () const { return im; }
private:
T re, im;
friend complex& __doapl (complex*, const complex&);
};
最后实例化
{
complex<double> c1(2.5,1.5);//这里为类模板使用时需要进行类型的指定,函数模板使用不需要指定类型
complex<int> c2(2,6);
...
}
4、inline——内联函数
函数在class body本体内定义,就形成一个inline
候选人,用 inline
关键字较好地解决了函数调用开销的问题,能否真正成为inline
函数由编译器决定。
以complex.h
为例,
class complex //任何类需要一个class head类头
{ //往下开始写class body
public:
complex (double r = 0, double i = 0): re (r), im (i){ }// body内直接定义,inline函数候选人
complex& operator += (const complex&); // 仅在此声明
double real () const { return re; } // body内直接定义,inline函数候选人
double imag () const { return im; } // body内直接定义,inline函数候选人
private:
double re, im;
friend complex& __doapl (complex*, const complex&);
};
在函数前加个inline
关键字会成为候选人,太过复杂的函数也不能成为真正inline
函数,如:
inline double imag(const complex& x) // inline 函数返回值类型 函数名()
{
return x.imag ();
}
二、构造函数
以complex.h
为例,
class complex //任何类需要一个class head类头
{ //往下开始写class body
public:
complex (double r = 0, double i = 0): re (r), im (i){ }// body内直接定义,inline函数候选人
complex& operator += (const complex&); // 仅在此声明
double real () const { return re; } // body内直接定义,inline函数候选人
double imag () const { return im; } // body内直接定义,inline函数候选人
private:
double re, im;
friend complex& __doapl (complex*, const complex&);
};
complex (double r = 0, double i = 0): re (r), im (i){ }
为构造函数,
1、构造函数要点:
- 函数名与类名相同
- 可以设置参数(该参数可以有默认值),如:
(double r = 0, double i = 0)
- 没有返回类型,不需要
构造函数特殊初始化语法: re (r), im (i)
complex (double r = 0, double i = 0): re (r), im (i){ }//推荐这种
/*等价于
complex (double r = 0, double i = 0){
re = r;
im = i;
}
*/
一个变量的赋值使用有两个阶段:1.初始化,2.赋值,使用。如果不用构造函数的特殊写法,就相当于跳过了初始化,直接在函数中赋值了,效率差。
与构造函数对应的为析构函数。不带指针的类,大多不用写析构函数。
2、函数可以有很多个——overloading(重载)
double real () const { return re; }
void real(double r) const { re = r; }
这两者函数名相同,但返回值类型,形参不同,在编译器看来是两个函数
对于下列的构造函数
complex (double r = 0, double i = 0): re (r), im (i){ }
complex () : re(0), im(0) { }
这样是不行的,因为这两个构造函数在没有形参的情况下,表达意思相同,如complex c1
既可以用构造1,也可以用构造2,编译器无法选择。
3、构造函数访问级别
构造函数一般放在public
中,放在private
中将无法被外界调用用于构造。
对于特殊情况
Singleton(单例模式)
独一份的,指此数据类型只能创建一份。此时是通过调用A类的函数,从而间接的创建对象
class A {
public:
static A& getInstance();
setup() { ... }
private:
A();// 构造函数 1
A(const A& rhs);// 构造函数 2
...
};
A& A::getInstance()
{
static A a;
return a;
}
4、常量成员函数const member functions
对于不会改变数据成员内容的函数,要加上const(在函数小括号后面,大括号的前面),这里就相当于告诉编译器,这是不会变的。
class complex //任何类需要一个class head类头
{ //往下开始写class body
public:
complex (double r = 0, double i = 0): re (r), im (i){ }// body内直接定义,inline函数候选人
complex& operator += (const complex&); // 仅在此声明
double real () const { return re; } // body内直接定义,inline函数候选人,加上const
double imag () const { return im; } // body内直接定义,inline函数候选人,加上const
private:
double re, im;
friend complex& __doapl (complex*, const complex&);
};
当产生实例方式如以下,方法一:
{
complex c1(2,1);
cout << c1.real();
cout << c1.imag();
}
无特殊变化,正确的
而对于方法二,(尤其是你的定义函数real()
和imag()
无const
关键字):
{
const complex c1(2,1);// 加上const
cout << c1.real();
cout << c1.imag();
}
会报错,编译器就会理解为这里是可能会变的,这就与const complex造成了矛盾,报错
三、参数传递
1、有三种参数传递方式:
- pass by value,传值,例如
complex (double r = 0, double i = 0): re (r), im (i){ }
- pass by reference,传引用,例如
operator << (ostream& os, const complex& x){}
- pass by reference to const,传引用const,例如
complex& operator += (const complex&);
pass by value会将参数值整个传进去,为了提高效率,尽可能使用pass by reference引用传递参数, 可以避免对参数的复制。
如果在函数内不想对传递变量进行修改,那么就要传引用const。
返回值传递方式同上,尽量使用by reference形式返回,如果要返回的值只是这个函数中的一个局部变量/临时变量,那就一定要用传值来返回。
什么情况下可以pass or return by reference?
- 当你要return or pass 的值,在调用该函数之前就存在空间来存储,那么就可以使用by reference
- 当你要return or pass 的值,是在函数内部新声明出来的(局部变量/临时变量),那么不可以使用
2、友元friend
被设置成友元的函数,就可以直接调取私有数据,这比通过公有函数效率更高一点。
class complex //任何类需要一个class head类头
{ //往下开始写class body
public:
complex (double r = 0, double i = 0): re (r), im (i){ }// body内直接定义,inline函数候选人
complex& operator += (const complex&); // 仅在此声明
double real () const { return re; } // body内直接定义,inline函数候选人,加上const
double imag () const { return im; } // body内直接定义,inline函数候选人,加上const
private:
double re, im;
friend complex& __doapl (complex*, const complex&);
};
在friend complex& __doapl (complex*, const complex&)
函数中,可以直接调用re
,im
的数据,无需通过real ()
和imag ()
,如下:
inline complex& __doapl (complex* ths, const complex& r){
ths->re += r.re;
ths->im += r.im;
return *ths;
}
相同class的各个对象互为友元,这句话就可以解释下面这个用法为何是成立的。也可以说,在类定义内可以访问其他对象的私有变量。
class complex{
public:
complex (double r = 0, double i = 0): re (r), im (i){ }
int func(const complex& param){ return param.re + param.im; }
private:
double re, im;
};
// 调用
{
complex c1(2,1);
complex c2;
c2.func(c1); //c2可以直接使用c1的re,im
}
四、操作符重载与临时对象
1、对于成员函数
所有的成员函数(声明在class内部的)一定带着一个隐藏参数this
指针, 这个this(指针)指向调用这个函数的调用者。代码中参数部分不可以写这个this,但是在函数内部可以用this。传送者无需知道接收者是以什么形式接受
例如:
inline complex& __doapl(complex* ths, const complex& r){
ths->re += r.re;
ths->im += r.im;
return *ths;
}
inline complex& complex::operator += (const complex& r)
{
return __doapl (this, r);
}
// 调用
{
complex c1(2,1);
complex c2(5);
c2 += c1;// 对于+=操作符,会把c2的地址传到this指针中
c3 += c2 += c1; // 对于上图中的complex::operator += ,其返回类型设置为complex&,是为了可以处理连续赋值的情况:若不处理连续的+=,可以将返回值设置为void
}
2、对于非成员函数
对全局函数重载:没有this情况下:
inline complex operator + (const complex& x, const complex& y){
return complex (real (x) + real (y),imag (x) + imag (y));
}
inline complex operator + (const complex& x, double y){
return complex (real (x) + y, imag (x));
}
inline complex operator + (double x, const complex& y){
return complex (x + real (y), imag (y));
}
// 为了应付三种加法,这里要写三种函数。注意这里的返回值一定不能用引用,因为return的都是临时变量。
// 形如typename(),为临时对象,在下一行失效,如:complex(a,b);
//当只有一个参数的时候:这里表示正负号
inline complex operator + (const complex& x){ // 这里应该可以return by reference
return x;
}
inline complex operator - (const complex& x){
return complex (-real (x), -imag (x));
}
对于特殊的操作符<<
,要把操作符重载设计成全局函数。因为ostream
可能是很早之前就定义好的,我们无法提前知道他要输出的类型,不可能预先在成员函数中定义。
#include <iostream.h>
ostream& operator << (ostream& os, const complex& x)
{
return os << '(' << real (x) << ','<< imag (x) << ')';// 这里的输出在改变os的状态,所以参数上不能加const
}
// 这里的operator << 的返回类型也是考虑到连续输出,所以返回ostream&
五、复习complex类实现过程
1、防卫式定义
#ifndef __COMPLEX__
#define __COMPLEX__
#endif
其中__COMPLEX__
为自定义名称
2、定义class head
#ifndef __COMPLEX__
#define __COMPLEX__
class complex
{
};
#endif
3、确定私有变量
#ifndef __COMPLEX__
#define __COMPLEX__
class complex
{
private:
double re, im;
};
#endif
4、确定构造函数
#ifndef __COMPLEX__
#define __COMPLEX__
class complex{
public:
complex (double r = 0, double i = 0): re (r), im (i){ }
private:
double re, im;
};
#endif
5、考虑具体函数实现功能
在考虑其他函数(以实现特定功能)时,注意这里可以是成员函数或全局函数(根据需要加inline
),思考要不要给函数加const
,是否要在private
加入友元函数,考虑好函数的接口(参数,返回值的类型),再考虑定义
#ifndef __COMPLEX__
#define __COMPLEX__
class complex{
public:
complex (double r = 0, double i = 0): re (r), im (i){ }
complex& operator += (const complex&);
double real () const { return re; }
double imag () const { return im; }
private:
double re, im;
friend complex& __doapl (complex*,const complex&);
};
// 成员函数
inline complex& __doapl(complex* ths, const complex& r){
ths->re += r.re;
ths->im += r.im;
return *ths;
}
inline complex& complex::operator += (const complex& r){ // 类名::函数名,表示该函数在该类下
return __doapl (this, r);
}
// 全局函数
inline complex operator + (const complex& x, const complex& y){
return complex ( real (x) + real (y),imag (x) + imag (y) );
}
inline complex operator + (const complex& x, double y){
return complex (real (x) + y, imag (x));
}
inline complex operator + (double x, const complex& y){
return complex (x + real (y), imag (y));
}
#endif
#include <iostream.h>
ostream& operator << (ostream& os,const complex& x){
return os << '(' << real (x) << ','<< imag (x) << ')';
}
六、实现对有指针成员的类string
//string.h 防卫式声明
#ifndef __MYSTRING__
#define __MYSTRING__
class String{
...
};
String::function(...) ...
Global-function(...) ...
#endif
//string-test.cpp
int main(){
String s1(),
String s2("hello");
String s3(s1);// 实现了拷贝构造
cout << s3 << endl;
s3 = s2; // 实现了拷贝赋值
cout << s3 << endl;
}
最后呈现的string类的class body
class String{
public:
String(const char* cstr = 0);
String(const String& str);
String& operator=(const String& str);
~String();
char* get_c_str() const { return m_data; }
private:
char* m_data;
};
七、三大函数
1、析构函数
形如~类名()
为析构函数,与构造函数相对应
以string类为例,如下:
inline String::String(const char* cstr = 0){// string类中的一种构造函数
if (cstr) {
m_data = new char[strlen(cstr)+1];
strcpy(m_data, cstr);
} else { // 未指定初值
m_data = new char[1];
*m_data = '\0';
}
}
inline String::~String(){ // string类中的析构函数
delete[] m_data;// delete[]要与new[]相互搭配
}
{
String s1(),
String s2("hello");//当s1,s2离开作用域时,析构函数会被调用
String* p = new String("hello");
delete p;// 释放字符串p
}
必须要有拷贝构造和拷贝赋值的原因:
String a("Hello");
String b("World");
此时则有两个指针,a-> H e l l o \0 , b -> w o r l d \0
执行b = a;
若不写拷贝构造和拷贝赋值,则会采用系统默认的拷贝,最后结果:
a-> H e l l o \0
b-> H e l l o \0
a和b同时指向H e l l o \0,此时,w o r l d \0没有指针指向,无法调用访问,就产生了内存泄漏
这种也叫做浅拷贝,只拷贝了指针
2、拷贝构造
也为深拷贝,拷贝内容
inline String::String(const String& str){ //函数名与类名相同,收到的参数是自己的类
m_data = new char[ strlen(str.m_data) + 1 ]; //直接取另一個object 的private data.兄弟之間互為friend
strcpy(m_data, str.m_data);
}
{
String s1("hello ");
String s2(s1); // String s2 = s1;
}
3、拷贝赋值
inline String& String::operator=(const String& str){
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;
}
{
String s1("hello ");
String s2(s1);
s2 = s1;
}
分为四步:
- 检测自我赋值(对于自己给自己赋值的情况,如果不加这一步就会出错)
- 删除原有的指针成员的全部内存 (主要是删除了这一步)
- 重新给指针成员分配内存大小,和要赋值的内容一样
- 赋值
八、堆heap,栈stack与内存管理
栈:是存在于某作用域(scope)的一块内存空间。例如当你调用函数,函数本身就会形成一个stack用来放置它所接收的参数以及返回地址。在函数本体内声明的任何变量,其所使用的内存块都取自上述stack。
堆:是指由操作系统提供的一块global内存空间,程序可动态分配从其中获得若干区块。
class Complex{...};
...
{
Complex c1(1,2); //c1所占的空间来自栈,当程序离开该作用域时,生命自动消失
Complex* p = new Complex(3); //Complex(3)是临时对象,其占用的空间是以new自堆动态分配而得,并由p指向,需要手动释放
}
1、stack objects的生命期
class Complex { ... };
...
{
Complex c1(1,2);
}
c1就是stack object, 生命在作用域结束之后结束,这种作用域内的object又称为auto object,因为能被自动清除
2、static local objects 的生命期
class Complex { ... };
...
{
static Complex c2(1,2);
}
c2就是static object, 生命在作用域(大括号范围)结束之后仍然存在,直到整个程序结束
3、global objects 的生命期
class Complex { … };
...
Complex c3(1,2);
int main(){
...
}
c3为global object, 生命在整个程序结束后才结束,也可以视为一种static object,作用域是整个程序
4、heap objects 的生命期
class Complex { … };
...
{
Complex* p = new Complex;
...
delete p;
}
p指向heap object, 生命在它被delete之后结束。
如退出作用域时,没有delete,那p指针指向的区域仍存在,而p指针已经死亡,导致内存泄漏
5、使用new和delete时的内存分配过程
new
先分配内存再调用构造函数
Complex* pc = new Complex(1,2);
//编译器转化为
Complex *pc;
void* mem = operator new( sizeof(Complex) ); //分配內存,其內部調用malloc(n)
pc = static_cast<Complex*>(mem); //转型
pc->Complex::Complex(1,2); //构造函數
delete
先调用析构函数再释放内存
Complex* pc = new Complex(1,2);
...
delete pc;
//编译器转化为
Complex::~Complex(pc); // 析构函數
operator delete(pc); // 释放內存,其內部調用free(pc)
九、复习string类实现过程
1、定义class head 以及防卫式声明
#ifndef __MYSTRING__
#define __MYSTRING__
class String
{
};
#endif
2、定义私有变量
#ifndef __MYSTRING__
#define __MYSTRING__
class String
{
private:
char* m_data;
};
#endif
3、确定构造函数
#ifndef __MYSTRING__
#define __MYSTRING__
class String{
public:
String(const char* cstr=0);
private:
char* m_data;
};
#endif
4、确定三大函数
#ifndef __MYSTRING__
#define __MYSTRING__
class String{
public:
String(const char* cstr=0);
String(const String& str); // 拷贝构造
String& operator=(const String& str); // 拷贝赋值
~String(); // 析构函数
char* get_c_str() const { return m_data; }
private:
char* m_data;
};
#endif
5、补充函数实现功能
#ifndef __MYSTRING__
#define __MYSTRING__
class String{
public:
String(const char* cstr=0);
String(const String& str); // 拷贝构造
String& operator=(const String& str); // 拷贝赋值
~String(); // 析构函数
char* get_c_str() const { return m_data; }
private:
char* m_data;
};
#include <cstring>
inline String::String(const char* cstr){
if (cstr) {
m_data = new char[strlen(cstr)+1];
strcpy(m_data, cstr);
}
else { // 未指定初值
m_data = new char[1];
*m_data = '\0';
}
}
inline String::~String(){ // 析构函数
delete[] m_data;
}
inline String& String::operator=(const String& str){ // 拷贝赋值,string& 表示传引用
if (this == &str) return *this; //检测自我赋值, &str表示得到str的地址,是一个指针
delete[] m_data; // 删除原有空间
m_data = new char[ strlen(str.m_data) + 1 ]; // 开辟现需内存
strcpy(m_data, str.m_data); // 赋值
return *this;
}
inline String::String(const String& str){ //拷贝构造
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
}
#include <iostream>
using namespace std;
ostream& operator<<(ostream& os, const String& str){
os << str.get_c_str();
return os;
}
#endif
十、补充
1、static
在变量或函数前添加static关键字,成为静态变量或静态函数
// 在没有static关键字时,complex类有data members 和 member functions
complex c1,c2,c3;
cout << c1.real(); // cout << complex::real(&c1); 这里this指针指向&c1
cout << c2.real(); // cout << complex::real(&c2); 成员函数real()只有一个,是通过this指针,指向谁,来处理谁,这里this指针指向&c2
// 所以完整的real()写法,如下
class complex
{
public:
double real () const { return this->re; }// 这里的this可写可不写
private:
double re, im;
};
当添加static关键字后,static member data或 static member function就会与对象脱离,在内存有一块单独的区域
static member function没有this指针,所以无法像一般的member function 去处理对象,只能去处理static member data
以银行账户为例
class Account {
public:
static double m_rate; //只是声明,静态变量
static void set_rate(const double& x) { m_rate = x; }
};
double Account::m_rate = 8.0; //赋初值定义,必须在类的外头
/*
调用static 函數的方式有二:
(1) 通过object 调用
(2) 通过class name 调用
*/
int main() {
Account::set_rate(5.0);//通过class name 调用
Account a;
a.set_rate(7.0); //通过object 调用
}
2、构造函数放private
在**Singleton(单例模式)**中,用static关键字表明,该类独一份
class A {
public:
static A& getInstance( return a; );//静态函数,取得唯一的对象
setup() { ... } //A::getInstance().setup();通过这样获取setup函数
private:
A();//构造函数
A(const A& rhs);//构造函数
static A a;// static表明独一份
...
};
进一步优化,当需要对象A时在创建
class A {
public:
static A& getInstance();
setup() { ... }
private:
A();
A(const A& rhs);
...
};
A& A::getInstance(){//只有别人调用这个函数时,才会生成静态变量a,且只有一份
static A a;
return a;
}
3、模板 template
类模板class template
template<typename T> class complex{
public:
complex (T r = 0, T i = 0): re (r), im (i){ }
complex& operator += (const complex&);
T real () const { return re; }
T imag () const { return im; }
private:
T re, im;
friend complex& __doapl (complex*, const complex&);
};
//使用
{
complex<double> c1(2.5, 1.5);// 明确指出typename是什么
complex<int> c2(2, 6);
...
}
函数模板function template
template <class T> inline const T& min(const T& a, const T& b){
return b < a ? b : a;
}
//此时我有一个stone类,如下:
class stone{
public:
stone(int w, int h, int we): _w(w), _h(h), _weight(we){ }
bool operator< (const stone& rhs) const { return _weight < rhs._weight; }
private:
int _w, _h, _weight;
};
//进行min比较
stone r1(2,3), r2(3,3), r3;
r3 = min(r1, r2);// 编译器会对function template 进行引数推导(argument deduction),引数推导的結果,T为stone,于是调用stone::operator<
4、namespace
语法:关键字+名称
namespace std//std为包裹标准库所有东西的空间
{
...//这里内容全部被包裹在一个命名空间里面
}
使用方法:
-
使用命令using directive,全部开放标准库
-
#include <iostream.h> using namespace std; int main(){ cin << ...; cout << ...; return 0; }
-
使用声明using declaration,只开放部分标准库
-
#include <iostream.h> using std::cout;//声明了cout int main(){ std::cin << ...;//对于未声明的,只能写完整全名 cout << ...;//可以不写全名 return 0; }
-
未声明
十一、组合与继承
面向对象类和类的关系:
- Inheritance (继承)
- Composition (复合)
- Delegation (委托)
1、Composition (复合),表示has-a
以下列为示例,queue类中拥有 deque类,左边(Container) 拥有右边(Component),Container和Component两者生命是一起出现的。
template <class T> class queue {//队列
...
protected :
deque<T> c; //底层容器
public:
// 以下完全利用 c 的操作函数完成
bool empty() const { return c.empty(); }
size_type size() const { return c.size(); }
reference front() { return c.front(); }
reference back() { return c.back(); }
//
void push(const value_type& x) { c.push_back(x); }
void pop() { c.pop_front(); }
}
从构造函数来看,
由内而外:外部的 Container的构造函数首先调用内部的Component的default构造函数,然后才执行自己。(红色部分编译器自动加的,使用构造函数特殊语法,当默认构造函数不满足需求时,要自己写)
从析构函数来看,
由外而内: 外部的Container的析构函数首先执行自己,然后才调用内部的Component的析构函数。
2、Delegation (委托),Composition by reference
左边(Handle)(拥有指针)指向右边(Body),两者以指针相连,不是同时存在的,只有左边用到右边时,右边才存在。
左边handle
// file String.hpp
class StringRep;
class String {
public:
String();
String(const char* s);
String(const String& s);
String &operator=(const String& s);
~String();
. . . .
private:
StringRep* rep; // pimpl,指向右边的指针
};
右边body
// file String.cpp
#include "String.hpp"
namespace {
class StringRep {
friend class String;
StringRep(const char* s);
~StringRep();
int count;
char* rep;
};
}
String::String(){ ... }
...
3、Inheritance (继承),表示is-a
语法:class 子类 :public 父类,其中public也可以改为private,protected
struct _List_node_base//父类
{
_List_node_base* _M_next;
_List_node_base* _M_prev;
};
template<typename _Tp>
struct _List_node : public _List_node_base //子类
{
_Tp _M_data;
};
子类会继承父类的数据、函数,其中最有价值的地方是和虚函数打通
两者关系图如下,base为父类,derived为子类
从构造函数来看,由内而外,子类的构造函数首先调用父类的默认构造函数,然后才执行自己
从析构函数来看,由外而内,子类的析构函数首先执行自己,然后才调用父类的析构函数,父类的析构函数,必须是 virtual
,否则会出现undefined behavior
Inheritance(继承)+Composition(复合)关系下的构造和析构
- 构造由内而外: Derived(子类)的构造函数首先调用Base(父类)的default构造函数,然后调用Component的default构造函数,然后才执行自己。(红色部分编译器自动加的!)
- 析构由外而内: Derived(子类)的析构函数首先执行自己,然后调用Component的析构函数,然后才调用Base(父类)的析构函数
十二、虚函数与多态
1、虚函数
non-virtual 函数:你不希望子类重新定义(override, 覆写) 它.
virtual 函数:你希望子类重新定义(override, 覆写) 它,且你对它已有默认定义。
pure virtual 函数:你希望子类 一定要重新定义(override 覆写)它,你对它没有默认定义。
class Shape {
public:
virtual void draw( ) const = 0;//pure virtual,形式就长这样
virtual void error(const std::string& msg);//impure virtual
int objectID( ) const;//non-virtual
...
};
class Rectangle: public Shape { ... };
class Ellipse: public Shape { ... };
在重写虚函数时,所有的关键字、函数名、参数都要一样
下列例子包含了继承+委托
父类:
class Component
{
int value;
public:
Component(int val) { value = val; }
virtual void add( Component* ) { } // 虚函数
};
子类1:
class Primitive: public Component
{
public:
Primitive(int val): Component(val) {}
};
子类2:
class Composite: public Component
{
vector <Component*> c;
public:
Composite(int val): Component(val) { }
void add(Component* elem) { // 重写虚函数
c.push_back(elem);
}
…
};
2、多态
Image父类
#include <iostream.h>
enum imageType
{
LSAT, SPOT
};
class Image
{
public:
virtual void draw() = 0;
static Image *findAndClone(imageType);
protected:
virtual imageType returnType() = 0;
virtual Image *clone() = 0;
// As each subclass of Image is declared, it registers its prototype
static void addPrototype(Image *image)
{
_prototypes[_nextSlot++] = image;
}
private:
// addPrototype() saves each registered prototype here
static Image *_prototypes[10];
static int _nextSlot;
};
Image *Image::_prototypes[];//静态变量要定义
int Image::_nextSlot;
// Client calls this public static member function when it needs an instance of an Image subclass
Image *Image::findAndClone(imageType type)
{
for (int i = 0; i < _nextSlot; i++)
if (_prototypes[i]->returnType() == type)
return _prototypes[i]->clone();
}
LandSatImage子类
class LandSatImage: public Image
{
public:
imageType returnType() {
return LSAT;
}
void draw() {
cout << "LandSatImage::draw " << _id << endl;
}
// When clone() is called, call the one-argument ctor with a dummy arg
Image *clone() {
return new LandSatImage(1);
}
protected:
// This is only called from clone()
LandSatImage(int dummy) {
_id = _count++;
}
private:
// Mechanism for initializing an Image subclass - this causes the
// default ctor to be called, which registers the subclass's prototype
static LandSatImage _landSatImage; //静态的自己
// This is only called when the private static data member is inited
LandSatImage() {
addPrototype(this);
}
// Nominal "state" per instance mechanism
int _id;
static int _count;
};
// Register the subclass's prototype
LandSatImage LandSatImage::_landSatImage;
// Initialize the "state" per instance mechanism
int LandSatImage::_count = 1;
SpotImage子类
class SpotImage: public Image
{
public:
imageType returnType() {
return SPOT;
}
void draw() {
cout << "SpotImage::draw " << _id << endl;
}
mage *clone() {
return new SpotImage(1);
}
protected:
SpotImage(int dummy) {
_id = _count++;
}
private:
SpotImage() {
addPrototype(this);
}
static SpotImage _spotImage;
int _id;
static int _count;
};
SpotImage SpotImage::_spotImage;
int SpotImage::_count = 1;
main
// Simulated stream of creation requests
const int NUM_IMAGES = 8;
imageType input[NUM_IMAGES] =
{
LSAT, LSAT, LSAT, SPOT, LSAT, SPOT, SPOT, LSAT
};
int main()
{
Image *images[NUM_IMAGES];
// Given an image type, find the right prototype, and return a clone
for (int i = 0; i < NUM_IMAGES; i++)
images[i] = Image::findAndClone(input[i]);
// Demonstrate that correct image objects have been cloned
for (i = 0; i < NUM_IMAGES; i++)
images[i]->draw();
// Free the dynamic memory
for (i = 0; i < NUM_IMAGES; i++)
delete images[i];
}
十三、转换函数(conversion function)
1、转出去
把class A 转成另一种class,语法:operator+空格+另一种类型
,以分数这类为例:
class Fraction {
public:
Fraction(int num, int den = 1) :m_numerator(num), m_denominator(den) {}
operator double() const { // 转换函数,没有参数,不用写返回类型,通常加上const,表示不改变
return ((double)m_numerator / m_denominator);
}
private:
int m_numerator; //分子
int m_denominator;//分母
};
Fraction f(3,5);
double d = 4 + f;//调用operator double()将f转为0.6
编译器先去查找是否有支持的函数能让这行代码通过,首先找全局函数是否有operator+(double,Fraction)
,重载了+号,发现没有,则看能否将f转换为double。找到了operator double() const
,于是f变成了0.6。
2、转过来
把别的class转成class A,使用non-explicit-one-argument构造函数,
class Fraction
{
public:
Fraction(int num,int den = 1):m_numerator(num),m_denominator(den){}//该构造函数是,只有一个实参的构造函数,且没有explicit关键字
Fraction operator+(const Fraction& f){
return Fraction(...);
}
private:
int m_numerator;
int m_denominator;
};
Fraction f(3,5);
double d = 4 + f;//调用non-explicit 构造器将4转为Franction(4,1),然后调用operator+
在Fraction类中我们重载了+运算符,可以使两个Fraction对象进行相加,但是对于一个整数与一个Fraction对象进行相加,于是编译器去看看能不能将4转换为Fraction,如果可以转换,则符合了我们的+重载,于是调用构造函数Fraction(int num,int den = 1),将4转换为Fraction,进行加法
3、转换冲突
将上面两个例子中的两个成员函数整合。
class Fraction
{
public:
Fraction(int num,int den = 1):m_numerator(num),m_denominator(den){}
operator double()const {
return ((double)m_numerator / m_denominator);
}
Fraction operator+(const Fraction& f){
return Fraction(...);
}
private:
int m_numerator;
int m_denominator;
};
Fraction f(3,5);
double d = 4 + f;// error 有歧义
解决方法:
给构造函数添加explict关键字,explicit多数用在构造函数处,少数还有在模板处
class Fraction
{
public:
explicit Fraction(int num,int den = 1):m_numerator(num),m_denominator(den){}
operator double()const {
return ((double)m_numerator / m_denominator);
}
Fraction operator+(const Fraction& f){
return Fraction(...);
}
private:
int m_numerator;
int m_denominator;
};
给构造函数添加explict关键字,此时别的类无法转换为Fraction对象,转过来的路走不通,只能转出去
Fraction f(3,5);
double d = 4 + f;
f被转成double进行计算,结果为4.6
注:不能写成double d = f + 4;
,因为explicit的关系,4不能转换成Fraction对象,会报错
十四、类指针类(pointer-like classes)
设计一个类,让它的行为像指针
1、智能指针
智能指针就是一种经典的“像一个指针的类”,这个class创建的对象像指针,要比普通指针多做一些事情
智能指针中一定有一个一般的指针(下图中,大的圆圈为智能指针,中间的点为一般指针)
它一定有 * 、—>
这两个操作符的重载,且实现手法都是固定的
注:"->"
这个符号很特别
- 例如说,上图右侧中的
* sp
中的,* 号,在使用后就会消失。 - 但是
sp->method()
,我们可以看到,调用sp->
在右侧的类中,返回px
,再往下看px->method()
,会发现,这里其实少了一个->
,这里就体现出这个符号的特殊性了,得到的东西会继续用箭头符号作用上去。
2、迭代器
迭代器是另一种pointer-like classes的指针
迭代器中一定有一个一般的指针
除了有 * 、—>
这两个操作符的重载,还有++、--
等等其他操作符的重载。且 * 、—>
重载的实现手法与智能指针不一样。
上图为链表的示例,其中迭代器为图中的泡泡,
十五、仿函数(function-like classes)
设计一个类,让它的行为像函数
一个函数需要()
操作符,即function call operator,如果一个东西能接受()
操作符,那么我们就把这个东西叫做函数,或像函数的东西。
以上为标准库的例子,以第二个为例,使用select1st
方式为:select1st<Pair> () ()
,第一个()
为创建临时对象,第二个()
才是调用操作符。
标准库中,有很多仿函数,它其实是一个很小的类,这些仿函数都继承了一些“奇怪”的父类,这些“奇怪”父类的大小理论上为0,且没有函数,只有一些typedef定义。(详见标准库课程)
十六、成员模板(member template)
也就是模板的嵌套,模板中有模板,如下图黄色部分
黄色这一块是当前模板的一个成员,同时它自己也是个模板。所以它就叫做成员模板
T1,T2可以变化,U1,U2也可以变化。
在STL标准库中会大量出现成员模板,先来一个小示例:
这里T1为鱼类,U1为鲫鱼,T2为鸟类,U2为麻雀
鲫鱼类继承自鱼类,麻雀类继承自鸟类
使用鲫鱼和麻雀构成的pair,然后拷贝到到鱼类和鸟类构成的pair,这样是可以的。反之则不行。允许或不允许限制的条件为: 下方代码中的构造函数。(父类指针可以指向子类对象)
下面为另一个例子
父类指针可以指向子类对象,可以实现up-cast为向上构造
十七、模板特化(specialization)
泛化的反面就是特化,特化就是根据特定的类型进行特殊处理
// 泛化
#include <iostream>
using namespace std;
// 泛化
template<class Key>
struct hash1{};
// 特化
template<>
struct hash1<char>{
size_t operator()(char x)const {
return x;
}
};
template<>
struct hash1<long>{
size_t operator()(long x)const {
return x;
}
};
int main(void){
// 调用
hash1<long>()(1000);// 构造一个hash的临时对象,传递参数1000,找到上面的特化long
return 0;
}
以上为全特化,与之相对应的是偏特化(局部特化)
偏特化一个是从个数上的偏,另一个是从范围上的偏
首先,个数的偏
即将模板中的某个/些参数提前进行”绑定“,从左边开始绑定,不能跳
范围上的偏
将什么类型都可以传的模板,偏特化为只有指针类型能传的模板。(当然,指针可以指向任意类型,但指针也是一种类型,所以范围确实变小了),从接收任意范围T,到接收指针T*,算是对范围上的特化
十八、模块模板参数(tempalte template parameter)
模板的参数又是一个模板,语法就是下面黄色的部分,如:
如上图所示,传递任意的容器与元素类型进行组合,其中第一个打岔的部分,光看语法上并没有问题,但是,实际上在我们定义容器list的时候有多个默认参数,这样做是无法通过编译的,如果只有一个默认参数,那么是可以的。
下图中,黄色部分其实不是模板模板参数
调用中我们使用第二种方法,指明第二模板参数,其实这个list< int >就已经不是模板了,已经指明了,即使它是用模板设计出来的东西。
但是已经绑定,写死,list中的元素类型为int;注意与上一张图对比,所以temp<class T,class Sequence = deque< T >>第二个参数,不是模板模板参数。
十九、可变参数模板(variadic templates-since)
语法就是typename...
、Types&...
、args...
,表示参数数量可变,如果你想确定这“一包”参数具体有多少个,可以用语法:sizeof...(args)
#include <iostream>
#include <bitset>
using namespace std;
void print(){}
template<typename T,typename... Types>
void print(const T& firstArg,const Types&...args) {//参数分为两组,第一组只有一个参数,第二组参数个数不知道
cout<<firstArg<<endl;
print(args...);// 调用,将这一包拆开
// 注意: 到最后变成0个的时候,将会调用上面的print()
cout<<sizeof...(args)<<endl;// 获得这一包中有几个元素
}
int main(void){
print(7.5,"hello",bitset<16>(377),42);//调用
return 0;
}
二十、自动类型推导(auto)
auto其实就是一个语法糖,面对复杂的返回类型可以用auto自动推导出来。
示例:
list<string>c;
...
list<string>::iterator ite;
ite = find(c.begin(),c.end(),target);
其中list<string>::iterator ite;
,太过复杂,可以使用auto,改为:
list<string>c;
...
auto ite = find(c.begin(),c.end(),target);
//以下为错误例子
auto ite;// 编译器不能也无法知道这个ite是什么,无法进行推导
ite = find(c.begin(),c.end(),target);
二十一、更简洁的for(ranged-base for)
范围for循环,也是C++11的一个语法糖。它有如下特点:
- 它有两个参数,一个是自己创建的变量,另一个是一个容器。
- 范围for循环可以将一个容器(第二个参数)里的元素依次传第一个参数,并在该循环体中依次对每一个元素做操作。
- 如果你不想影响容器中的参数,请pass by value,否则请pass by reference。
示例:
vector<double>vec;
//...
// pass by value 传值,里面的操作不会改名vec里原来的东西
for(auto elem:vec){
cout<<elem<<endl;
}
// pass by reference 传引用——改变vec原来的东西
for(auto& elem:vec){
elem *=3;
}
二十二、引用(reference)
编译器其实把reference视作一种pointer。引用有如下特点:
- 引用是被引用对象的一个别名。
- 引用一定要有初始化。
- object和其reference的大小相同,地址也相同(全都是假象)
- reference通常不用于声明变量,而**用于参数类型(parameters type)和返回类型(return type)**的描述。
- 在函数调用的时候,pass by value和pass by reference传参形式一样,而且reference比较快,所以推荐一般pass by reference。(请回顾何时不可以pass by reference)
int x = 0;
int* p = &x;// p为指针、类型为point to int,初值为x的地址,指向x,写法上推荐*靠近int,之后可以修改指向
int& r = x;// r类型为reference to int,代表x,从此之后,r不能代表其他,x现在为0,所以r也为0,现在r不是指针,x现在为int,所以r也为int
int x2 = 5;
r = x2;// r不能重新代表其他,现在x r都为5,相当于将值5赋给x
int& r2 = r; //r2 = 5,r2代表r,也相当于代表x
注意:
sizeof(r)== sizeof(x)
相等&x == &r
相等
因为这是编译器制造出的假象,大小相同,地址也相同,举例验证如下:
typedef struct Stag {int a, b, c, d;} S;
int main(){
double x = 0;
double* p = &x; //p指向x,p的值是x的地址
double& r = x; //r代表x,现在r、x的值都是0
cout << sizeof(x) << endl; //8
cout << sizeof(p) << endl; //4,指针在32位电脑里为4个字节
cout << sizeof(r) << endl; //8
cout << p << endl; //0065FDFC
cout << *p << endl; //0
cout << x << endl; //0
cout << r << endl; //0
cout << &x << endl; //0065FDFC
cout << &r << endl; //0065FDFC
S s;
S& rs = s;
cout << sizeof(s) << endl; //16
cout << sizeof(rs) << endl; //16
cout << &s << endl; //0065FDE8
cout << &rs << endl; //0065FDE8
}
referece 就是一种漂亮的pointer,多用于参数传递——传引用
注意以下两者不能共存:
double imag(const double& im){...}
double imag(const double im){...}
因为两者的same signature相同,即 imag(const double& im) = imag(const double im)
,如果改为imag(const double& im) const
,那么两者same signature不同,即imag(const double& im) const ≠ imag(const double im)
,可以共存
二十三、虚指针(vptr)和虚表(vtbl)
现有三个类ABC,三者之间为继承的关系,继承父类函数,继承的是调用权
请注意:A B C三个类的非虚函数,即func1、func2
,他们虽然重名,但其实彼此之间毫无关系,这一点要注意,千万别以为B的非虚函数是继承A的非虚函数。
B类的内存大小= 继承A类的数据 + B本身的数据,C类同理。(关系图最右边)
A有两个虚函数vfunc1
、vfunc2
以及两个非虚函数func1
、func2
;
B类继承A类的vfunc2
,同时覆写A类的vfunc1
,此时B有两个虚函数(vfunc1和vfunc2)
;
C类继承了B类的vfunc2
(vfunc2
其实是A的),同时覆写了vfunc1
,也有两个虚函数。
所以A B C这三个类一共有八个函数,四个非虚函数,四个虚函数。(关系图中间偏右)
只要一个类拥有虚函数,则就会有一个虚指针vptr,该vptr指向一个虚表vtbl,虚表vtbl中放的都是函数指针,指向虚函数所在的位置。(可以观察到,关系图中虚表中的函数指针都指向相应的虚函数的位置)这其实就是动态绑定的关键。
如果创建一个指向C类的指针p(如C* p= new C),如果让该指针p调用虚函数C::vfunc1(),则编译器就知道这是动态绑定,故这时候就会通过p找到vptr,再找到vtbl,最终通过vtbl的函数指针找到相应的虚函数。该步骤如果要解析成C语言的话就是图片中灰框所示,其中n指的是要调用的虚函数在虚表中的第几个。n在写虚函数代码的时候编译器看该虚函数第几个写的则n就是几。
与虚函数相关的就是多态
例如一个容器,其元素是指针类型,它的功能是要画出不同的形状,所以要用虚函数,子类要进行覆写实现自己的形状绘制。调用虚函数的过程为动态绑定——即多态,父类指针可以接收具体的子类对象,即根据具体是哪个子类,调用该虚函数具体的形式。
总结一下,C++编译器看到一个函数调用,它有两个考量:
- 是静态绑定吗?(Call ×××)
- 还是动态绑定。
要想动态绑定要满足三个条件:
- 第一:必须是通过指针来调用
- 第二:该指针是向上转型(up-cast)的
- 第三:调用的是虚函数
到此为止,多态、虚函数、动态绑定是指的一回事了
二十四、this
类的成员函数中,默认会有一个this指针传递进来。由编译器自己处理
当你通过一个对象调用一个函数,该对象的地址就是this指针。
二十五、Dynamic Binding动态绑定
现在考虑一下,为什么动态绑定解析成C语言形式会是:(*(p->vptr)[n])(p)
或(* p->vptr[n])(p)
,第二个p其实就是this指针。
从汇编角度看一下:
下图中 a是一个对象,它调用函数是一个静态绑定,可以看到汇编呈现的就是:Call xxx一个地址
下图中pa满足动态绑定的三个条件,所以它是一个动态绑定,而在汇编语言中,汇编所呈现出来的那部分命令就等价于C语言中的(*(p->vptr)[n])(p)
二十六、const
const用于修饰成员函数——即放到成员函数参数列表后,表明该成员函数不打算修改成员变量的值
- const也是函数签名的一部份,即可构成函数重载。
- 常量对象不可以调用非常量成员函数。
- 当成员函数的const和non-const版本同时存在,const对象只能调用const版本的成员函数,non-const对象只能调用non-const版本的成员函数。
- non-const成员函数可调用const成员函数,反之则不行
二十七、重载Operator new,operator delete
全局重载
在前面加上::
,表示全局重载,即**::operator new, ::operator delete, :: operator new[], ::operator delete[]
**
重载member operator new/delete
如下所示,重载一个类中的成员,这里注意一下delete重载的第二个参数时可选的,可以不写。
重载member operator new[] / delete[]
具体的例子,如下:
重载new()
要求:
- 示例:
Foo* pf = new(300,'c') Foo;
- 可以重载多个class member operator new()版本,但每一个版本的参数列表必须独一无二。
- 且参数列表的第一个参数必须为size_t,其余参数为使用时()中指定的参数。出现在new(…)小括号内的便是所谓的placement arguments,示例中的300,‘c’
- 所以上述的使用形式小括号内虽然看到有两个参(300,‘c’),其实有三个。
重载delete()
要求:
- 可以(也可以不)重载多个class member operator delete()版本,但绝不会被delete调用(这个delete是指可以被分解为两步的那个delete)
- 唯一被调用的时机:只有当new所调用的构造函数(new被分解的第一步)抛出异常,才会调用与new对应的那个重载operator delete(),主要用来归还未能完全创建成功的对象所占用的内存
placement new的应用:Basic_String使用new(extra)扩充申请量
标准库中对placement new的一个应用,用于无声无息的扩充申请一部份内存。