关于STL这部分,原课程将其分为了四部分,我做笔记时,会将其整合,使其更具有整体性
文章目录
Ⅰ C++ part1 面向对象编程
1 头文件与类的声明
1.1 c vs cpp关于数据和函数
c语言中,data和函数都是分别定义,根据类型创建的。这样创建出的变量,是全局的
cpp中,将数据data和函数都包含在一起(class),创建出一个对象,即为面向对象;数据和函数(类的方法)都是局部的,不是全局的
class的两个经典分类:
- 无指针成员的类(complex)——复数
- 有指针成员的类(string)——字符串
1.2 头文件与类
1.2.1 头文件
引用自己写的头文件,用双引号
头文件的标准写法:
complex.h:
#ifndef _COMPLEX_ // 如果没有被定义过就定义 (防卫式声明)
#define _COMPLEX_
#endif
- 首先是防卫式声明,如果没定义这个名词,那么就定义一下。ifndef+define。(这样如果程序是第一次引用它,则定义,后续则不需要重复定义,不需要重复进入下面的过程)
1
要写的类的声明,2
是要写类的具体定义,写1
2
的时候发现有一些东西需要提前声明,写在0
处
1.2.2 class的声明
在C++中 struct和class唯一的区别就在于默认的访问权限不同
- struct 默认权限为公共
- class 默认权限为私有
class complex //class head
{ //class body /*有些函数直接在此定义,另一些在 body 之外定义*/
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&);
};
{
complex c1(2,1);
complex c2;
...
}
1.2.3 模板初识
{
complex<double> c1(2.5, 1.5);
complex<int> c2(2, 6);
...
}
- 因为实部和虚部的类型不确定,可能是
double
float
int
,定义一个模板类型叫做T
- 将
T
作为一个类型参数来传入,在调用的时候就可以指定类型了 - 通过在定义类的前面加入一行代码
template<typename T>
来实现
2 构造函数
2.1 inline 函数
定义类的时候,可以直接在body中定义函数(inline函数,在body中定义完成),也可以只是在body中声明函数
- inline内联函数:如果定义的函数是内联函数,那么会运行比较快,尽可能定义为内联函数
- 在body外,通过
inline
关键字来指定该函数为inline函数。
注意的是,上面所有的inline函数,都只是我们指定的,希望它为inline,具体是不是,要看编译器来决定
2.2 访问级别
-
数据应该被定义为private
-
函数要被外界使用,定义为public;若只是内部处理,定义为private
2.3 ctor 构造函数
2.3.1 ctor 的写法
方式一:(推荐)
complex(T r = 0, T i = 0) //函数名称与class的名称一致
: re(r), im(i) //中间这一行就是初始化
{ }
方式二:(不推荐)
complex(double r = 0, double i = 0)
{
re = r; im = i; //用赋值来进行初始化
}
通过构造函数来创建对象。会自动调用构造函数进行创建。
- 构造函数名称需要与类的名称一样
- 函数的参数可以有默认参数
- 构造函数没有返回类型
2.3.2 ctor/函数 重载
构造函数可以有很多个,可以重载;但是上面的1
2
两个构造函数冲突了
complex c2(); // "()" 可以不要,一样的
上面的调用方式对两个构造函数都适用,冲突
double real () const { return re; }
void real (double r) { re = r; } //不能有const
- 同名的函数可以有多个,编译器会编成不同的名称,实际调用哪个会根据哪个适用
2.3.3 ctor 放在 private 区
- 通常构造函数不要放在private中,这样外界没法调用,也就无法创建对象
- 在设计模式
Singleton
(单体)中,将构造函数放在了private中;这个class只有一份,外界想要调用的时候,只能使用定义的getInstance()
函数来取得这一份;外界无法创建新的对象
2.4 const 常量成员函数
对于不会改变数据内容的函数,一定要加上const
{
const complex c1(2, 1);
cout << c1.real();
cout << c1.imag();
}
对于上面调用方式,我们创建一个常量复数然后调用函数输出实部虚部,如果上面real和imag函数定义的时候,没有加const,那么这里函数默认的意思是可能会改变数据,与我们的常量复数就矛盾了,编译器会报错;因此,对于不会改变数据内容的函数,一定一定要加const
3 参数传递与返回值——引用
3.1 参数传递
-
值传递 pass by value,传递value是把整个参数全传过去,尽量不要直接value传递 例
double r
-
引用传递 pass by reference,传引用相当于传指针,快,形式也漂亮 例
complex&
-
如果只是为了提升速度,不向改变数据,那么传const引用;这样传进去的东西,不能被修改
例
const complex&
3.2 返回值传递
返回值的传递,尽量返回引用
在函数中创建的变量 (local 变量),要返回——这种情况是不能返回引用的;因为函数结束后函数中创建的变量就消失了,无法引用
传递者无需知道接受者是以reference形式接受——所以用reference形式很便捷
4 友元 friend
4.1 友元
友元:friend,修饰在函数定义之前,表示这个函数可以直接拿该类对象的private数据
inline complex&
__doapl(complex* ths, const complex& r)
{
ths->re += r.re; //直接拿private的数据,不需要函数
ths->im += r.im;
return *ths;
}
- 如上面所示,声明为friend之后,函数可以直接取到re和im,如果不被声明为friend,只能通过调用real和imag函数来得到,效率较低
4.2 相同 class 的 object 互为 friends
{
complex c1(2, 1);
complex c2;
c2.func(c1);
}
相同class的不同对象互为友元,即可以直接取另一個 object 的 private data
5 操作符重载与临时对象
5.1 操作符重载
在c++里我们可以定义加法等操作符,比如我们可以定义两个石头的加法
5.1.1 成员函数实现 / this
成员函数: complex :: function ....
前面带有class的名称(在class里先声明了的)
inline complex&
complex::operator += (const complex& r) {
return __doapl(this, r); //do assignment plus
}
所有的成员函数都带有一个隐藏的参数this
(是一个指针),this
指向调用这个函数的调用者
-
定义函数的时候,在参数中不能写出来
this
,直接用即可 -
函数里可写可不写,但当传入参数与成员变量名相同时要写
public: double real () const { return this->re; } //这里的this->可省略
c3 += c2 += c1; // c2 加了 c1 后如果返回 void 就无法进行 c3 的操作了
将操作符写为void函数也可以,但为了可以兼容c3+=c2+=c1
的形式,写成返回引用更好。
5.1.2 非成员函数实现
非成员函数没有this
应对三种使用方法,写出三种方式
-
非成员函数是global函数——为了后面两种使用方法
-
这些函数不能返回引用,必须值传递
在函数中创建的新变量 (local 变量),要返回
5.1.3 output函数 << 的重载
cout不认识新定义的这种复数,因此也需要对<<
进行操作符重载
只能全局函数,不能成员函数——导致使用时方向相反
#include <iostream.h>
ostream&
operator<<(ostream& os, const complex& x)
{
return os << '(' << real(x) << ',' << imag(x) << ')'; //自定义输出
}
ostream&
是cout
的 classname
参数传递:os 在函数中会变化,所以不能加
const
返回值传递:为了避免
cout << c1 << conj(c1);
连续输出,不用void
cout << c1
返回值需要与cout
类型一致
5.2 临时对象
classname ()
创建一个classname类型的临时对象——不需要名称,生命只有一行
6 带指针的类:三大函数
-
析构函数:
~String();
-
拷贝构造函数 copy ctor :
String (const String& str);
——string s3(s1)
-
拷贝赋值函数 copy op= :
String& operator=(const String& str);
——s3=s2
编译器默认的拷贝构造赋值(一个bit一个bit的复制),编译器默认的只是拷贝了指针(浅拷贝),而不是指针指向的数据
alias(别名)和 memory leak(内存泄漏)都是十分危险的
因此,如果类中有指针,一定自己写这两个函数
6.1 ctor 和 dtor (构造和析构函数)
6.1.1 ctor 构造函数
这里的 new
是申请的字符串的空间
inline
String::String(const char* cstr = 0)
{
if (cstr) { // 指定了初值—— String s2("hello");
m_data = new char[strlen(cstr) + 1]; // 字符串长度 + /0
strcpy(m_data, cstr);
}
else { // 未指定初值—— String s1();
m_data = new char[1];
*m_data = '\0';
}
}
这里的 new
是申请的指针的空间,String()
里面还有一个 new
String* p = new String("hello");
delete p;
6.1.2 dtor 析构函数
inline
String::~String()
{
delete[] m_data;
}
每个 new
都对应一个 delete
—— 一定要释放
类对象死亡的时候(离开作用域),析构函数会被自动调用
例:这里结束会调用三次 dtor
{
String s1(),
String s2("hello");
String* p = new String("hello");
delete p;
}
6.2 copy ctor 拷贝构造函数
inline
String::String(const String& str)
{
m_data = new char[strlen(str.m_data) + 1]; // “str.m_data” 兄弟之间互为友元
strcpy(m_data, str.m_data); // 深拷贝
}
String s1("hello ");
String s2(s1);
6.3 copy op= 拷贝赋值函数
-
先杀死调用者
-
重新申请指定大小的空间
-
复制字符串内容到调用者
inline
String& String::operator=(const String & str)
{
if (this == &str) // 检测自我赋值 self assignment
return *this;
delete[] m_data; // 第一步
m_data = new char[strlen(str.m_data) + 1]; // 第二步
strcpy(m_data, str.m_data); // 第三步
return *this;
}
一定要在开始就检测自我赋值,因为
a=a
时第一步delete
了后,会使第三步出现问题
7 堆,栈,内存管理
7.1 堆和栈
Stack 栈,是存在于某作用域 (scope) 的一块内存空间。
例如当你调用函数,函数本身即会形成一个 stack
用来放置它所接收的参数,以及返回地址;在函数本体 (function body) 内声明的任何变量其所使用的内存块都取自上述 stack
Heap 堆,或称为 system heap ,是指由操作系统提供的一块 global 内存空间,程序可动态分配 (dynamic allocated) 从中获得若干区块 (blocks)
可以用 new
来动态取得
在 stack 中的是自动生成的空间,作用域结束空间会自动释放
在 heap 中的是自己申请的空间,需要自己释放
{
complex c1(1,2);
/*c1空间来自stack*/
complex* p = new complex(3);
/*complex(3) 是个临时对象
其所用的空间是以new从heap动态分配而得,并由p指向*/
}
7.2 object 生命期
-
stack objects 的生命期
c1
便是所谓 stack object,其生命在作用域 (scope) 结束之际结束这种作用域内的 object,又称为 auto object,因为它会被“自动”清理(结束自动调用析构函数){ complex c1(1,2); }
-
static local objects 的生命期
若在前面加上
static
后,其会存在到整个程序结束{ static complex c2(1,2); }
-
global objects 的生命期
写在任何作用域之外的对象,其生命在整个程序结束之后才结束,你也可以把它视为一种 static object,其作用域是整个程序
... complex c3(1,2); int main() { ... }
-
heap objects 的生命期
p
所指的便是 heap object,其生命在它被delete
之际结束{ complex* p = new complex; ... delete p; }
7.3 new 和delete
7.3.1 new
new:先分配 memory , 再调用 ctor
- 分配内存:先用一个特殊函数,按 class 的定义分配了两个
double
的大小 - 转型(忽视)
- 调用构造函数,赋值
(1,2)
7.3.2 delete
delete:先调用 dtor, 再释放 memory
- 调用析构函数——释放的是
m_date
指向的字符串Hello
的空间(即构造函数中new
申请的空间) - 释放内存:用一个特殊函数释放了
ps
指向的空间(即String* ps = new String("Hello");
中new
申请的空间)
7.4 内存动态分配
7.4.1 在VC下内存动态分配
在VC下(不同编译器的内存动态分配可能不同)
-
调试模式:
(4*3)
是3个指针的大小(32+4)
是调试模式所需空间(橘色部分)(4*2)
是上下两个 cookie ——表示内存块的开始与结束4
是数组才有的长度记录由于分配内存块需要是16的倍数,所以需要 pad 来填充到
64
-
执行模式:
去掉调试模式的空间即可
因为内存块是16的倍数,因此最后四位bit一定都是0,cookie 就借用最后的一位
1
表示占用内存,0
表示释放内存如上图
41h
中1
即表示占用内存
7.4.2 array new/delete
array new
一定要搭配 array delete
new
后有[ ]
—> delete
后加[ ]
普通的delete只调用一次析构函数——剩下两个指针的指向的空间没有调用析构函数,内存泄漏
这种情况发生在有指针的类,但最好都这样写
8 静态 模板 namespace
8.1 static
对于非静态的函数和数据:
非静态的成员函数通过this
指针来处理不同的数据(一份函数—>多个对象)
对于静态的函数和数据:
静态函数没有this
,不能处理一般的数据,只能处理静态的数据
例1:
class Account
{
public:
static double m_rate; //静态变量的声明
static void set_rate(const double& x) { m_rate = x; } //静态函数
};
double Account::m_rate = 0; //静态变量的定义 一定要有
int main()
{
//调用静态函数法1——by class name
Account::set_rate(5.0);
//调用静态函数法2——by object
Account a;
a.set_rate(7.0); //静态函数与a无关/无this
}
例2:设计模式 Singleton
(单体)
- 构造函数放在private中,外界无法调用
- 设计了
getInstance
静态函数,来生成并返回唯一的一份
8.2 template
8.2.1 class template 类模板
T
来代替某种类型- 使用时
classname<type1> xxx
,编译器会把T
全部替换为type1
8.2.2 function template 函数模板
比较函数——任何类型都可以进行比较;T
来代替某种类型
应用时,不需要写某种类型——编译器自己会推导
8.3 namespace
对东西进行一个包装(不一定要一次性全写在一起,可分开包装在一起)
namespace name
{
...
}
-
用法一:using directive
#include <iostream> using namespace std; //直接把包装全打开 int main() { cin << ...; cout << ...; return 0; }
-
用法二:using declaration
#include <iostream> using std::cout; //只打开一条 int main() { std::cin << ...; //没打开要写全名 cout << ...; return 0; }
-
用法三:都写全名
#include <iostream> int main() { std::cin << ; std::cout << ...; return 0; }
9 复合 委托
9.1 Composition 复合
类似于c中结构里有结构——class里有class
deque
是一个已经存在的功能很多的类(两头进出的队列);利用deque
的功能来实现queue
的多种操作
该例只是复合的一种情况——设计模式 Adapter
9.1.1 复合下的构造和析构
-
构造是由内而外
Container 的构造函数,编译器会自动先调用 Component 的 default 构造函数,再执行自己
注意如果要调用 Component 的其他构造函数需要自己写出来
Container::Container(…): Component() { … };
-
析构是由外而内
Container 的析构函数会先执行自己,之后编译器调用 Component 的析构函数
9.2 Delegation 委托
委托就是 Composition by reference;即通过指针把任务委托给另一个类
复合中,内部和外部是一起出现的;而委托是不同步的
这是一个著名的设计模式——pimpl (pointer to implementation) 或者叫 “编译防火墙”
右边怎么变动都不会影响左边
reference counting 多个指针共享一个 “Hello”;但当a要改变内容时, 系统会单独复制一份出来给a来改,b和c依然在共享
10 继承与虚函数
10.1 Inheritance 继承
语法::public base_class_name
public
只是一种继承的方式,还有protect
,private
子类会拥有自己的以及父类的数据
-
public 继承(public inheritance): 在公有继承中,基类的 public 和 protected 成员的访问属性在子类中保持不变,子类可以访问基类的 public 成员和受保护成员,但不能直接访问基类的私有成员
通过子类的对象只能访问基类的 public 成员
通常用于"is-a"关系,表示派生类是基类的一种类型,派生类的对象可以替代基类的对象
-
protected 继承(protected inheritance): 在受保护继承中,基类的 public 和 protected 成员都以 protected 身份出现在子类中,基类的private 成员仍然是私有;子类成员函数可以直接访问基类中的 public 和 protected 成员,但不能直接访问基类的 private 成员
通过子类的对象不能直接访问基类中的任何成员
通常用于实现继承的实现细节,不表示"is-a"关系,而是表示派生类需要基类的实现,但不希望公开基类的接口
-
private 继承(private inheritance): 在私有继承中,基类的 public 和 protected 成员都以 private 身份出现在子类中,基类的私有成员仍然是私有;子类成员函数可以直接访问基类中的 public 和 protected 成员,但不能直接访问基类的 private 成员
通过子类的对象不能直接访问基类中的任何成员
基类 public 成员 | 基类 protected 成员 | 基类 private 成员 | |
---|---|---|---|
public 继承 | public 成员 | protected 成员 | 不能访问 |
protected 继承 | protected 成员 | protected 成员 | 不能访问 |
private 继承 | private 成员 | private 成员 | 不能访问 |
10.1.1 继承下的构造和析构
与复合下的构造和析构相似
-
构造是由内而外
Derived 的构造函数,编译器会自动先调用 Base 的 default 构造函数,再执行自己
注意如果要调用 Base 的其他构造函数需要自己写出来
Derived::Derived(…): Base() { … };
-
析构是由外而内
Derived 的析构函数会先执行自己,之后编译器调用 Base 的析构函数
Derived::~Derived(…){ … /* ~Base() */ };
注意:Base class 的 dtor 必需是 virtual
否则下例会导致结束时只会调用 Base 的 dtor
int main() { Base* ptr = new Derived(); delete ptr; // 只会调用 Base 类的析构函数 return 0; }
10.2 虚函数
-
pure virtual 函数:
derived class 一定要重新定义 (override 覆写) 它;其没有定义只有声明
语法:
virtual xxxxxx =0;
-
virtual 函数:
derived class 可以重新定义 (override, 覆写) 它,且它已有默认定义
语法:
virtual xxxxxx;
-
non-virtual 函数:
不希望 derived class 重新定义 (override, 覆写) 它
10.3 继承 with virtual
例子:在 Windows 平台下用某个软件打开文件——分为好几步,但基本所有软件大多数操作都是一致的,只有一个操作如读取方式是不一样的
- 现有一个框架 Application framework 其写好了所有必要的函数,其中
Serialize()
就是一个 pure virtual 函数 - 使用这个框架写自己软件的打开文件,就继承这个框架,其中就需要自己 override 覆写
Serialize()
这个函数 - 在执行中,执行
myDoc.OnFileOpen();
中到Serialize()
时,是通过this
来指引到自己写的Serialize()
中去的
把关键动作延缓到子类再做,这是一个经典的设计模式——Template Method
10.4 缩略图
-
复合:
-
委托:
-
继承:
-
类中的元素: 变量名称 : 变量类型(与代码刚好相反
-
变量下面加下划线 表示
static
-
前面加一个
-
表示private
-
前面加一个
#
表示protected
-
前面加一个
+
表示public
(一般可以省略)
-
10.5 继承+复合
这种关系下的构造和析构与之前的类似
-
第一种:
-
构造由内到外 先 Base 再 Component
Derived 的构造函数首先调用 Base 的 default 构造函数,然后调用 Component 的 default 构造函数,然后才执行自己
Derived::Derived(…): Base(),Component() { … };
-
析构由外而内 先 Component 再 Base
Derived 的析构函数首先执行自己,然后调用 Component 的析构函数,然后调用 Base 的析构函数
Derived::~Derived(…){… /*~Component() ~Base()*/};
-
-
第二种:
同理构造由内到外,析构由外而内
10.6 继承+委托
10.6.1 例一 Observer
设计模式—— Observer
例如一串数据,可以用饼图来观察,也可以用条形图来观察,这种种的观察方式都是继承于 Observer
通过 vector<Observer> m_views;
来进行委托
当数据改变的时候,Observer 也需要更新,即 notify
函数,来将目前所有的观察者更新
10.6.2 例二 Composite
设计模式—— Composite
例如文件系统,文件夹里可以有文件夹(与自己相同的类),也可以有文件,其中文件就是最基本的 Primitive,而文件夹就是复合物 Composite
要达成目的,就可以再设计一个父类 Component ,文件和文件夹就继承于同一父类;
其中 Composite 要用委托到父类的方式 Component*
设计容器和操作——使其 Primitive 和 Composite 都可以适用
//父类 Component
class Component
{
private:
int value;
public:
Component(int val) {value = val;}
virtual void add( Component* ) {} //虚函数
};
//复合物 Composite
class Composite
: public Component
{
vector <Component*> c;
public:
Composite(int val) : Component(val) {}
void add(Component* elem)
{
c.push_back(elem);
}
…
}
//基本类 Primitive
class Primitive
: public Component
{
public:
Primitive(int val): Component(val) {}
};
component中add是虚函数(且是空函数),不能是纯虚函数——Primitive 不会 override add函数(最基本的单位,不能 add 了),而 Composite 需要 override add函数
10.6.3 例三 Prototype
设计模式—— Prototype
框架(父类)要创建未来才会出现的子类——要求子类要创建一个自己当作原型 Prototype 让框架(父类)来找到并创建 FindAndClone
补充:当一个子类继承自父类时,它可以被视为是父类的一种类型,因此可以使用父类的指针或引用来引用子类的对象;
这种用父类的指针或引用来处理子类对象的方式称为——**向上转型 ** Upcasting
-
父类中,有一个存放原型的数组,有纯虚函数
Image *clone()
,还有两个静态函数Image FindAndClone(imageType);
void addPrototype(Image *image){...}
-
子类中,创建一个静态的自己
_LAST
,把它放到父类的一个空间中,这样父类就可以找到新创建的子类private 的构造函数
LandSatImage()
中是addPrototype(this); //这里的 this 就是 _LAST
将自己的原型放到了父类中去 -
子类中,准备一个
clone()
函数,父类通过调用找到的相应类型的 clone 函数来创建子类的副本这里的 clone 函数就不能用之前的那个构造函数来创建副本了——其会放到父类中去,所以创建一个新的构造函数
LandSatImage(int)
用传进一个无用参数(随便传个int型数据就好)来进行区分
Ⅱ C++ part2 兼谈对象模型
1 转换
1.1 转换函数
将当前对象的类型转换成其他类型
- 以
operator
开头,函数名称为需要转成的类型,无参数 - 前面不需要写返回类型,编译器会自动根据函数名称进行补充
- 转换函数中,分子分母都没改变,所以通常加
const
// class Fraction里的一个成员函数
operator double() const
{
return (double) (m_numerator / m_denominator);
}
Fraction f(3,5);
double d = 4 + f; //编译器自动调用转换函数将f转换为0.6
1.2 non-explicit-one-argument ctor
将其他类型的对象转换为当前类型
one-argument 表示只要一个实参就够了
// non-explicit-one-argument ctor
Fraction(int num, int den = 1)
: m_numerator(num), m_denominator(den) {}
Fraction f(3,5);
Fraction d = f + 4; //编译器调用ctor将4转化为Fraction
1.3 explicit
当上面两个都有转换功能的函数在一起,编译器调用时都可以用,报错
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) const
{
return Fraction(...);
}
private:
int m_numerator; // 分子
int m_denominator; // 分母
};
...
Fraction f(3,5);
Fraction d = f + 4; // [Error] ambiguous
one-argument ctor 加上 explicit
,表示这个 ctor 只能在构造的时候使用,编译器不能拿来进行类型转换了
...
explicit Fraction(int num, int den = 1)
: m_numerator(num), m_denominator(den) {}
...
Fraction f(3,5);
Fraction d = f + 4; // [Error] 4不能从‘double’转化为‘Fraction’
关键字
explicit
主要就在这里运用
2 xxx-like classes
2.1 pointer-like classes
2.1.1 智能指针
- 设计得像指针class,能有更多的功能,包着一个普通指针
- 指针允许的动作,这个类也要有,其中
*
,->
一般都要重载
template <typename T>
class shared_ptr
{
public:
T& operator*() const { return *px; }
T* operator->() const { return px; }
shared_ptr(T* p) : ptr(p) {}
private:
T* px;
long* pn;
};
在使用时,
*shared_ptr1
就返回*px
;但是
shared_ptr1->
得到的东西会继续用->
作用上去,相当于这个->符号用了两次
2.1.2 迭代器
以标准库中的链表迭代器为例,这种智能指针还需要处理 ++
--
等符号
node
是迭代器包着的一个真正的指针,其指向 _list_node
- 下图
*ite
的意图是取data
——即一个 Foo 类型的 object - 下图
ite->method
的意图是调用 Foo 中的函数 method
2.2 function-like classes
设计一个class,行为像一个函数
函数行为即 —— xxx()
有一个小括号,所以函数中要有对 ()
进行重载
template <class pair>
struct select1st ... // 这里是继承奇特的Base classes,先不管
{
const typename pair::first_type& // 返回值类型,先不管
operator()(const pair& x) const
{
return x.first;
}
};
...
//像一个函数一样在用这个类
select1st<my_pair> selector;
first_type first_element = selector(example_pair);
//还可以这样写,第一个()在创建临时对象
first_type first_element = select1st<my_pair>()(example_pair);
...
3 模板
3.1 类模板/函数模板
补充:只有模板的尖括号中<>,关键字 typename
和 class
是一样的
3.2 成员模板
它即是模板的一部分,自己又是模板,则称为成员模板
其经常用于构造函数
- ctor1 这是默认构造函数的实现;它初始化
first
和second
分别为T1
和T2
类型的默认构造函数生成的默认值 - ctor2 这是带参数的构造函数的实现;它接受两个参数
a
和b
,并将它们分别用来初始化first
和second
成员变量 - ctor3 这是一个==模板构造函数==,接受一个不同类型的
pair
对象作为参数;它允许从一个不同类型的pair
对象构造当前类型的pair
对象,在构造过程中,它将源pair
对象的first
和second
成员变量分别赋值给当前对象的成员变量,使其具有一定的灵活性和通用性
template <class T1, class T2>
struct pair
{
T1 first;
T2 second;
pair() : first(T1()), second(T2()) {} //ctor1
pair(const T1& a, const T2& b) : //ctor2
first(a), second(b) {}
template <class U1, class U2> //ctor3
pair(const pair<U1, U2>& p) :
first(p.first), second(p.second) {}
};
-
例一,可以使用 <鲫鱼,麻雀> 对象来构造一个 <鱼类,鸟类> 的pair
-
例二,父类指针是可以指向子类的,叫做 up-cast;智能指针也必须可以,所以其构造函数需要为==模板构造函数==
3.3 模板模板参数
即模板中的一个模板参数也为模板,下图黄色高亮部分
XCLs<string, list> mylist
中即表示:容器 list 是 string 类型的—— 创建一个 string 的链表;Container<T> c;
即表示list<srting> c;
但是这样
Container<T> c;
语法过不了,容器 list 后面还有参数,需要用中间框和下面框下一行的代码 —— c++11的内容
注:下面不是模板模板参数
class Sequence = deque<T>
是有一个初始值,当没指定时就初始为deque<T>
在要指定时,如最后一行中的
list<int>
是确切的,不是模板
4 specialization 特化
4.1 全特化 full specialization
模板是泛化,特化是泛化的反面,可以针对不同的类型,来设计不同的东西
- 其语法为
template<>
struct xxx<type>
template<>
struct hash<char>
{
...
size_t operator()(char& x) const {return x;}
};
template<>
struct hash<int>
{
...
size_t operator()(int& x) const { return x; }
};
- 这里编译器就会用
int
的那段代码;注意:hash<int>()
是创建临时变量
cout << hash<int>()(1000)
4.2 偏特化 partial specialization
4.2.1 个数上的偏
例如:第一个模板参数我想针对 bool
特别设计
注意绑定模板参数不能跳着绑定,需要从左到右
4.2.2 范围上的偏
例如:想要当模板参数是指针时特别设计
C<string> obj1; //编译器会调用上面的
C<string*> obj2; //编译器会调用下面的
5 三个C++11新特性
5.1 variadic templates
模板参数可变化,其语法为 ...
(加在哪看情况)
// 当参数pack里没有东西了就调用这个基本函数结束输出
void print() {
}
// 用于打印多个参数的可变参数模板函数
template <typename T, typename... Args>
void print(const T& first, const Args&... args) {
std::cout << first << " ";
print(args...); // 使用剩余参数进行递归调用
}
int main() {
print(1, "Hello", 3.14, "World");
return 0;
}
还可以使用 sizeof...(args)
来得到参数pack里的数量
5.2 auto
编译器通过赋值的返回值类型,自动匹配返回类型
注:下面这样是不行的,第一行编译器找不到返回值类型
auto ite; // error
ite = find(c.begin(), c.end(), target);
5.3 ranged-base for
for
循环的新语法,for(声明变量 : 容器)
,编译器会从容器中依次拿出数据赋值给声明变量中
for (decl : coll)
{
statement
}
//例
for (int i : {1, 3, 4, 6, 8}) // {xx,xx,xx} 也是c++11的新特性
{
cout << i << endl;
}
注意:改变原容器中的值需要 pass by reference
vector<double> vec;
...
for (auto elem : vec) //值传递
{
cout << elem << endl;
}
for (auto& elem : vec) //引用传递
{
elem *= 3;
}
6 多态 虚机制
6.1 虚机制
当类中有虚函数时(无论多少个),其就会多一个指针—— vptr 虚指针,其会指向一个 vtbl 虚函数表,而 vtbl 中有指针一一对应指向所有的虚函数
有三个类依次继承,其中A有两个虚函数 vfunc1()
vfunc2()
,B改写了A的 vfunc1()
,C又改写了B的 vfunc1()
,子类在继承中对于虚函数会通过指针的方式进行——因为可能其会被改写
继承中,子类要继承父类所有的数据和其函数调用权,但虚函数可能会被改写,所以调用虚函数是==动态绑定==的,通过指针 p
找到 vptr
,找到vtbl
,再找到调用的第n个虚函数函数——( *(p->vptr[n]) )(p)
编译器在满足以下三个条件时就会做==动态绑定==:
- 通过指针调用
- 指针是向上转型 up-cast ——
Base* basePtr = new Derived;
- 调用的是虚函数
编译器就会编译成 ( *(p->vptr[n]) )(p)
这样来调用
例如:用一个 Shape(父类)的指针,调用 Circle(子类)的 draw 函数(每个形状的 draw 都不一样,继承自 Shape)
多态:同样是 Shape 的指针,在链表中却指向了不同的类型
list<Shape*> Mylist
多态优点:代码组织结构清晰,可读性强,利于前期和后期的扩展以及维护
6.2 动态绑定
a.vfunc1()
是通过对象来调用,是 static binding 静态绑定
在汇编代码中,是通过 call 函数的固定地址来进行调用的
pa
是指针,是向上转型,是用其调用虚函数—— dynamic binding 动态绑定
在汇编代码中,调用函数的时候,蓝框的操作用 c语言 的形式即是 —— ( *(p->vptr[n]) )(p)
下面同理
7 reference、const、new/delete
7.1 reference
x
是整数,占4字节;p
是指针占4字节(32位);r
代表x
,那么r
也是整数,占4字节
int x = 0;
int* p = &x; // 地址和指针是互通的
int& r = x; // 引用是代表x
引用与指针不同,只能代表一个变量,不能改变
引用底部的实现也是指针,但是注意 object 和它的 reference 的大小是相同的,地址也是相同的(是编译器制造的假象)
sizeof(r) == sizeof(x) &x == &r
reference 通常不用于声明变量,用于参数类型和返回类型的描述
以下 imag(const double& im)
和 imag(const double im)
的签名signature 在C++中是视为相同的——二者不能同时存在
double imag(const double& im) /*const*/ {....}
double imag(const double im){....} //Ambiguity
注意:const 是函数签名的一部分,所以加上后是可以共存的
7.2 const
const
加在函数后面 —— 常量成员函数(成员函数才有):表示这个成员函数保证不改变 class 的 data
const object | non-const object | |
---|---|---|
const member function(保证不改变 data members) | ✔️ | ✔️ |
non-const member function(不保证 data members 不变) | ❌ | ✔️ |
COW:Copy On Write
多个指针共享一个 “Hello”;但当a要改变内容时, 系统会单独复制一份出来给a来改,即 COW
在常量成员函数中,数据不能被改变所以不需要COW;而非常量成员函数中数据就有可能被改变,需要COW
charT
operator[] (size_type pos)const
{
.... /* 不必考虑COW */
}
reference
operator[] (size_type pos)
{
.... /* 必须考虑COW */
}
函数签名不包括返回类型但包括
const
,所以上面两个函数是共存的
当两个版本同时存在时,const object 只能调用 const 版本,non-const object 只能调用 non-const 版本
7.3 new delete
7.3.1 全局重载
- 可以全局重载
operator new
、operator delete
、operator new[]
、operator delete[]
- 这几个函数是在 new 的时候,编译器的分解步骤中的函数,是给编译器调用的
注意这个影响非常大!
inline void* operator new(size_t size){....}
inline void* operator new[](size_t size){....}
inline void operator delete(void* ptr){....}
inline void operator delete[](void* ptr){....}
7.3.2 class中成员重载
- 可以重载 class 中成员函数
operator new
、operator delete
、operator new[]
、operator delete[]
- 重载之后,new 这个类时,编译器会使用重载之后的
class Foo
{
public:
void* operator new(size_t size){....}
void operator delete(void* ptr, size_t size){....} // size_t可有可无
void* operator new[](size_t size){....}
void operator delete[](void* ptr, size_t size){....} // size_t可有可无
....
}
// 这里优先调用 members,若无就调用 globals
Foo* pf = new Foo;
delete pf;
// 这里强制调用 globals
Foo* pf = ::new Foo;
::delete pf;
7.3.3 placement new delete
可以重载 class 成员函数 placement new operator new()
,可以写出多个版本,前提是每一个版本的声明有独特的传入参数列,且其中第一个参数必须是 size_t,其余参数出现于 new(.....)
小括号内(即 placement arguments)
Foo* pf = new(300, 'c') Foo; // 其中第一个参数size_t不用写
// 对应的operator new
void* operator new (size_t size, long extra, char init){....}
我们也可以重载对应的 class 成员函数 operator delete()
,但其不会被delete调用,只当 new 调用的构造函数抛出异常 exception 的时候,才会调用来归还未能完全创建成功的 object 占用的内存