文章目录
- 类:
- 抽象类:
- 类、类作用域:
- 类成员访问权限:
- 使用细节:
- 成员函数的修饰符const、mutable:
- const在类中的使用:
- mutable:
- this:返回自身对象的引用
- 构造函数(类中,一种特殊的成员函数;函数名和类名相同):
- 多个构造函数:
- explicit(避免隐式转换):
- 构造函数的执行可分为两个阶段(初始化阶段和计算阶段,且初始化阶段先于计算阶段):
- 构造函数的初始化列表:
- 对象拷贝(直接使用赋值号) :
- 拷贝构造函数:
- 拷贝函数的种类:
- 拷贝构造函数的作用:
- 调用时机:
- 注意事项:
- 拷贝赋值函数,即重载赋值=运算符的函数:
- 对象移动、移动构造函数、移动赋值运算符:
- 对象移动,A移动到B:
- 移动构造函数,作用:
- 移动赋值运算符,作用:
- 合成的移动操作(某些条件下,编译器能合成移动赋值运算符):
- 重载运算符:
- 算术运算符重载(+、-、*、/):
- 一元运算符重载(++、--、!、&、~、*):
- 重载<<运算符:
- 重载关系运算符(==、!=、>、>=、<、<=):
- 重载new&delete运算符:
- c++中的new/delete背后操作:
- 重载内存分配/释放new/delete函数:
- 重载new[]/delete[]运算符:
- 重载=、[]、()、->运算符:
- 重载赋值=运算符:
- 重载下标[]运算符:
- 重载圆括号()运算符:
- 重载->运算符:
- 不能重载的5个运算符(五大创世宝石):
- 细节问题:
- *函数调用运算符:
- 析构函数:
- 类型转换:
- 类型转换构造函数:
- 类型转换运算符(与类型转换构造函数正好相反):
- 类对象转换为函数指针:
- 类型转换的二义性问题:
- static成员:
- 定义静态成员变量(分配内存):
- 静态变量(保存在静态存储区):
- 静态成员变量:
- 静态成员函数:
- 非静态成员函数中,可以访问静态成员:
- 私有的静态成员,在类外无法访问:
- 类成员指针:
- 类成员变量指针:是指向类成员变量的指针
- 类成员函数指针:是指向类成员函数的指针
- 友元关系:
- 友元全局函数:
- 友元类:
- 友元成员函数:
- 优/缺点:
- 特点:
- 内部类和友元类的区别:
- 不可继承的类:
- 基类、派生类:
- 使用继承的场景:
- 继承(面向对象的核心):基类和派生类
- 成员遮蔽(成员函数遮蔽):
- 子类调用父类同名函数:
- 派生类对象模型:
- 包含多个组成部分:
- 基类和派生类的创建和销毁时机:
- 派生类对象初始化一个基类对象:
- 派生类中构造基类:
- *基类指针、虚析构函数、多态对象模型、虚函数/纯虚函数:
- 基类指针:
- 虚析构函数(基类的析构函数,一般写成虚析构函数):
- 多态性(虚函数专用):
- 有了虚函数,当基类指针
- 基类引用/指针,都可以使用多态:
- 类的普通成员函数和虚成员函数:
- 虚函数:
- 纯虚函数:
- 讨论使用虚函数的必要性:
- 继承构造函数:
- 多重继承(从多个父类中产生子类):
- 虚基类、虚继承:
- RTTI运行时类型识别(run time type identification):
- 类与类之间的关系:
- inheritance继承,表示is-a:
- composition复合/组合,表示has-a:
- delegation委托(又称Composition by reference):
- 两个类互相依赖时:
- 可调用对象、std::function类模板:
- 可调用对象(函数指针、函数对象、匿名函数lambda表达式):
- `std::bind()`绑定器:
- std::function类模板(“可调用对象包装器”):
- std::bind()与std::function()结合使用:
- c++11新标准:
- 新增long long/unsigned long long类型:
- 原始字面量:
- 统一的初始化列表initializer_list:
- 两种类型推导关键字auto、decltype:
- 自动推导类型auto(推导得到的类型会做任何变动):
- decltype关键字(推导得到的类型不会做任何变动):
- lambda表达式:
- explicit关键字:
- noexcept:不抛出异常
- final关键字(放在类名或虚函数名后):
- override关键字:
- 基于范围for语句的循环:
- 默认函数控制=default与=delete:
- 别名alias:
- 类型的别名:typedef / using
- 模板的别名:typedef / using
- 模板模板参数:
- 空指针nullptr(有更高的安全性):
- 智能指针:unique_ptr、shared_ptr、weak_ptr:
- 强枚类型举(枚举类):
- 数值类型和字符串类型之间的转换:
- 常量表达式constexpr关键字:
- 左值/右值,左值引用/右值引用:
- 委托构造和继承构造:
- 委托构造:
- 继承构造:
- 可变参数模板Variadic template:
- 资源管理方案RAII:
- 指针:
- 指针的概念:
- 指针的运算:
- c++中的几种原始指针:
- 野指针:
- 解决出现空指针、野指针及内存泄露的问题:
- 智能指针:
- new/delete:
- new[]/delete[]数组:
- 智能指针综述:
- c++标准库std::中,四种智能指针(都是类模板)
- auto_ptr(c++98):
- unique_ptr:独占式指针
- 初始化:
- unique_ptr常用操作:
- 删除器、尺寸:
- 指定删除器:
- 删除器额外说明:
- 尺寸问题:
- 注意:
- shared_ptr(类模板、强指针):共享式指针
- 初始化:
- shared_ptr的底层结构:
- shared_ptr工作机制的分析:
- 引用计数的增加:
- 引用计数的减少:
- 支持普通的拷贝和赋值:
- shared_ptr智能指针常用操作:
- shared_ptr提供了支持数组的特化版本:
- 指定删除器:
- 指定删除器以及数组问题:
- 指定删除器额外说明:
- shared_ptr在多线程环境中的安全性:
- 自定义简化的shared_ptr:
- weak_ptr(类模板、弱指针):用来辅助shared_ptr工作
- shared_ptr 相互引用时的死锁问题(存在循环引用):
- weak_ptr的底层结构(与shared_ptr相同):
- weak_ptr的创建:
- 弱引用(weak_ptr)的作用是:
- weak_ptr常规操作:
- 智能指针的删除器:
- 智能指针的主要目的:
- 智能指针的选择:
- 引用计数:
- c++的多态:
- 引入:
- 主要分为静态多态和动态多态:
- 多态的实现方式:
- 动态多态:
- 静态多态:
- 多态的应用:
类:
窥视C++细节-为什么成员初始化列表更快。
抽象类:
特征:类内至少有一个纯虚函数,且不能生成对象。
一般设计原则:
- 抽象类的构造函数和拷贝构造函数、析构函数,都是用protected修饰;
- 抽象类的析构函数设置为纯虚函数,且需在类外定义该纯虚函数的函数体;
类、类作用域:
类是一种作用域,在该作用域内定义变量、函数。
类的作用域外,
- 普通的成员只能通过“对象/对象指针/对象引用”访问;
- 静态成员可以通过对象访问,也可通过“类名::成员”来访问;
存在继承关系时,即基类的作用域嵌套在派生类中;
- 如果成员在派生类中找到,就不会在基类作用域中继续查找;否则,在基类作用域中查找;
- 如:B b; 调用基类A的val成员变量
b.A::val
; 调用派生类B的val成员变量b.val
;
类成员访问权限:
struct默认是public;class默认是private(class类的成员函数,可以访问成员变量,不用考虑是否是私有);
- public:被public修饰的类成员可以在任何地方被访问到;
- protected:被protected修饰的类成员可以在类内部,子类内部和友元函数访问到,但不能通过类对象访问;
- private:被private修饰的类成员可以在类内部和友元函数访问到,但不能在子类和通过类对象访问;
继承时的属性变化:
protected的作用:(子类继承时可以访问到)
基类中的某个成员函数,我们不想将其暴露,但又想在子类中能访问到,这时就可以使用protected修饰。
使用细节:
-
对象一般不用memset()清空成员变量,可以用一个构造函数来完成成员变量的初始化。
-
直接在类定义中实现的成员函数,会被当做inline函数来处理(能否inline成功,取决于成员函数是否简单)。
-
函数形参列表的默认值:
- 默认值只能放在函数的声明中,除非该函数没有函数声明;
- 在具有多个参数的函数中,指定默认值时,默认参数都必须出现在非默认参数的右边;
-
一般使用结构体描述纯粹的数据,用类描述对象;
-
如果类的成员也是类,创建对象时先构造成员类;销毁对象时,先析构成员类;
-
c++编译器可能会给自动给类添加的四个成员函数:
- 默认构造函数,空实现。
- 默认析构函数,空实现。
- 默认拷贝构造函数,对成员变量进行浅拷贝。
- 赋值运算符,对成员变量进行浅拷贝。
- 合成的移动操作(某些条件下,编译器能合成“移动构造函数/赋值运算符”)。
-
拷贝构造和拷贝复制函数,使用细节注意:
- 如果需要多个不允许拷贝的类,不推荐直接把各个拷贝函数delete掉,建议各个类继承自一个不允许拷贝的类!
#include<iostream> using namespace std; class NonCopyable { public: NonCopyable()=default; NonCopyable(const NonCopyable&)=delete; NonCopyable &operator=(const NonCopyable&)=delete; }; class B : public NonCopyable {}; class C : public NonCopyable {}; int main() { A a; B b1; //B b2 = b1; // Error:此时要完成子类B对象的拷贝,必须先调用父类NonCopyable对象的拷贝构造函数,但父类已被delete,故会error return 0; }
- 继承关系下,子类的拷贝构造和拷贝赋值,编译器并不会自动调用父类的拷贝构造和拷贝赋值,需要手动调用。
#include<iostream> #include<cstring> using namespace std; class A { private: int m_a1; char* m_a2; // 指向定长10bytes的内存 public: A() : m_a1(0), m_a2(new char[10]) { cout << "A::A()" << endl; } A(const A& a) { m_a1 = a.m_a1; m_a2 = new char[10]; memcpy(m_a2, a.m_a2, 10); cout << "A::A(const A& a)" << endl; } A& operator=(const A& a) { cout << "A& A::operator=(const A& a)" << endl; // 避免“自我赋值” if (this == &a) { return *this; } delete m_a2; m_a2 = new char[10]; memcpy(m_a2, a.m_a2, 10); m_a1 = a.m_a1; return *this; } }; class B : public A { public: B() : A() { cout << "B::B()" << endl; } /* 这里需要手动调用 */ // 如果初始化列表中,不手动调用A的拷贝构造函数,则编译器会默认调用A() B(const B& b) : A(b) { cout << "B::B(const B& b)" << endl; } B& operator=(const B& b) { A::operator=(b); // 这里需要手动调用 cout << "B& B::operator=(const B& b)" << endl; return *this; } }; int main() { B b1; B b2 = b1; // 先调用A的拷贝构造函数,再调用B的拷贝构造函数 b2 = b1; // 先调用A的拷贝赋值运算符,再调用B的拷贝赋值运算符 return 0; } /* 子类的拷贝复制运算符中,必须手动调用父类的拷贝赋值运算符 */ /* 子类的拷贝构造函数,在初始化列表中手动调用父类的拷贝构造函数 */ A::A() B::B() A::A(const A& a) B::B(const B& b) A& A::operator=(const A& a) B& B::operator=(const B& b) /* 子类的拷贝构造函数,未手动调用父类的拷贝构造函数 */ A::A() B::B() A::A() B::B(const B& b) A& A::operator=(const A& a) B& B::operator=(const B& b)
-
拷贝构造函数中参数设置为值传递,会导致无限递归调用,最终导致栈溢出。
-
拷贝构造一定要const修饰,为了兼容处理两种场景(传入的被拷贝对象是const和非const类型)。
- 常量对象只能调用const修饰的成语函数。
- 如果拷贝构造函数不加const修饰,则无法进行“拷贝构造”,只能调用默认构造或有参构造。
-
在构造对象时,调用的是拷贝构造函数;给已有对象赋值时,调用拷贝赋值运算符。
#include <iostream> #include <cstring> using namespace std; class A { public: int* m_data = nullptr; // 指向堆区资源的指针,类内初始化 public: A() = default; // 启用默认的构造函数 void alloc() { m_data = new int; // 分配堆区内存 memset(m_data, 0, sizeof(int)); // 将分配的内存初始化为0 } A(const A& a) // 拷贝构造函数:形参必须为“常量引用类型” { cout << "A(cosnt A& a)" << endl; if (m_data == nullptr) { alloc(); } memcpy(m_data, a.m_data, sizeof(int)); } A& operator=(const A& a) // 拷贝赋值函数 { cout << "A& operator=(const A& a)" << endl; if (this == &a) { return *this; } // 避免"自我赋值" if (m_data == nullptr) { alloc(); } memcpy(m_data, a.m_data, sizeof(int)); return *this; } ~A() { delete m_data; cout << "~A()" << endl; } A(A&& a) // 移动构造函数,形参不能用const修饰,因最后要将a.m_data置空 { cout << "A(const A&& a)" << endl; if (m_data != nullptr) // 如果已分配内存,则先释放掉 { delete m_data; } m_data = a.m_data; // 将源对象中的指针指向的内存地址,赋值给新对象中的指针 a.m_data = nullptr; // 将源对象中的指针置空 } A& operator=(A&& a) { cout << "A&& A(A&& a)" << endl; if (this == &a) // 避免“自我赋值” { return *this; } if (m_data != nullptr) // 如果已分配内存,则先释放掉 { delete m_data; } m_data = a.m_data; // 将源对象中的指针指向的内存地址,赋值给新对象中的指针 a.m_data = nullptr; // 将源对象中的指针置空 return *this; } }; int main() { A a1; a1.alloc(); *(a1.m_data) = 3; cout << *(a1.m_data) << endl; A a2 = a1; // 调用拷贝构造函数 cout << *(a2.m_data) << endl; A a3; a3 = a2; // 调用拷贝赋值函数 cout << *(a3.m_data) << endl; cout << ".............." << endl; A a4(std::move(a1)); // 调用移动构造函数 A a5 = std::move(a2); // 调用移动构造函数 A a6; a6 = std::move(a3); // 调用移动赋值函数 }
成员函数的修饰符const、mutable:
const在类中的使用:
const成员变量初始化,必须在构造函数的初始化列表里进行,不可以通过赋值来初始化。
成员函数末尾加const修饰:
-
使用规范:成员函数声明和定义中,结尾都必须加上const。
-
const只能放在成员函数的末尾,不能放在普通非成员函数的末尾。
-
作用:const修饰的成员函数内部不能修改任何成员变量的值。
-
成员函数间的调用问题:
1)非const成员函数,既能调用const成员函数,也能调用非const成员函数;
2)const成员函数,只能调用const成员函数;
对象调用成员函数的问题:
- 非const对象,既可以调用const成员函数,也能调用非const成员函数;
- const对象,只能调用const成员函数;
mutable:
- mutable的引入,就是为了突破const的限制;
- 用mutable修饰一个成员变量,这个成员变量永远处于可以被修改的状态,即const修饰的成员函数中,mutable成员也可以被修改;
this:返回自身对象的引用
编译器负责把存放了对象地址的this指针,作为隐藏参数传递给成员函数(非静态成员函数);
- this指针指向调用成员函数的对象;*this表示对象;
系统角度看,任何对类成员的直接访问都被看做是通过this做隐式调用的;
注意事项:
-
this指针,只能在成员函数中使用,全局函数、静态函数均不能使用this指针;
-
普通成员函数中,this是一个指向“非const对象的const指针”(类为Time,则
this ==> Time *const this
,表示this只能指向当前的Time对象);const成员函数中,this指针是一个指向const对象的const指针(类为Time,则
this ==> const Time *const this
);this是常量指针,即成员函数内部不能改变this保存的地址); -
如果成员函数的形参和成员变量重名,那么在该成员函数中使用成员变量时,必须 加
this->
; -
用空指针可以调用没有用到this指针的非静态成员函数。
class Stu { public: func() // 该成员函数没有用到this指针 { cout << "..." << endl; } } // 用空指针可以调用没有用到this指针的非静态成员函数 Stu* stu = nullptr; stu->func(); //一旦非静态成员函数中用到了this指针,用空指针调用则会使程序崩溃
注意:为了避免调用非静态成员函数时,传入的this为空指针,而导致程序崩溃,则需要在使用到this指针的非静态成员函数开头,验证this是否为
nullptr
。
构造函数(类中,一种特殊的成员函数;函数名和类名相同):
在创建类对象时,系统会自动调用该成员函数,可理解为:构造函数的目的就是初始化类对象的成员。
正常情况下,构造函数应该被声明为public,且构造函数没有返回值。
在构造函数名后直接加括号和参数不是调用构造函数,是创建匿名对象。
构造函数若有多个参数,在创建类对象时,要带上这些参数:
Student stu(17, "lisi");
Student stu(12, "wangwu");
// c++11支持统一的初始化列表
Student stu = {17, "wowo"};
Student stu{17, "wowo"};
Student stu = new Student{17, "wowo"};
默认构造函数编译器可以自动给定,但不做任何操作。
- 类的定义中,如果没有构造函数的情况下,编译器会自动定义一个默认构造函数(无参)。
- 一旦自己写了构造函数,不管构造函数带有几个参数,编译器都不会为我们创建默认构造函数。
拷贝构造函数编译器也可以自动给定,且会将已有的成员变量进行拷贝。
多个构造函数:
- 一个类中,可以有多个构造函数,就可以为类对象的创建提供多种初始化方法。
- 多个构造函数必须在参数数量或参数类型上有区别。
- 如果构造函数含有一个参数,则又称为转换构造函数。
explicit(避免隐式转换):
- 在构造函数声明中,加入
explicit
,使得构造函数只能用于初始化和显式类型转换。 - 一般单参数的构造函数,声明为
explicit
,避免隐式类型转换。
构造函数的执行可分为两个阶段(初始化阶段和计算阶段,且初始化阶段先于计算阶段):
- 初始化阶段:全部成员都会在初始化阶段初始化,即使该成员没有出现在构造函数的初始化列表中(没有初始化列表,系统会随机给于初始化列表值);
- 计算阶段:一般指用于执行构造函数体内的赋值操作;
构造函数的初始化列表:
Time::Time(int tempHour, int tempMIn, int tempSec) : Hour(tempHour), MInute(tempMin), Second(tempSec)
-
初始化列表、赋值,本质区别:
成员是类,使用初始化列表调用的是拷贝构造函数;赋值则会先创建对象(调用默认构造函数),然后赋值。因此,初始化列表对性能略有提升。
#include <iostream> using namespace std; class Teacher { public: string name; int age; public: Teacher() : name(""), age(0) { cout << "default constructor" << endl; } Teacher(string m_name, int m_age) : name(m_name), age(m_age) { cout << "with the constructor" << endl; } Teacher(const Teacher& teacher) : name(teacher.name), age(teacher.age) { cout << "copy constructor" << endl; } ~Teacher() { cout << "destructor" << endl; } }; class Stu { public: int age; string name; Teacher teacher; public: Stu() : age(0), name("") { teacher.name = ""; teacher.age = 0; cout << "default constructor" << endl; } // 先调用Teacher的拷贝构造函数,再执行该Stu类的有参构造函数;效率更高; Stu(string m_name, int m_age, const Teacher& m_teacher) : age(m_age), name(m_name), teacher(m_teacher) { cout << "with the constructor" << endl; } /* // 先调用Teacher的默认构造函数,再赋值,再执行该Stu类的有参构造函数 Stu(string m_name, int m_age, const Teacher& m_teacher) : age(m_age), name(m_name) { teacher.name = m_teacher.name; teacher.age = m_teacher.age; cout << "with the constructor" << endl; } */ ~Stu() { cout << "destructor" << endl; } }; int main() { Teacher teacher("yoyo", 25); Stu stu("li", 17, teacher); return 0; }
初始化列表相比赋值,更高效。
注意:
- 如果成员已经在初始化列表中,则不应该在构造函数中再次赋值;
- 初始化列表的括号中,可以是具体的值、构造函数的形参名、表达式;
- 如果成员变量是常量和引用,则必须使用初始化列表,因为常量和引用只能在定义时初始化;
- 必须使用初始化列表初始化的场景:(初始化列表与类内初始化,前者的优先级更高)
- 类成员为const类型
- 类成员为引用类型
- 没有默认构造函数的类类型
- 类存在继承关系,派生类必须在其初始化列表中调用基类的构造函数
- 默认构造函数:无参的构造函数
- 类的定义中,如果没有构造函数的情况下,编译器会自动定义一个默认构造函数(无参)
- 一旦自己写了构造函数,不管构造函数带有几个参数,编译器都不会为我们创建默认构造函数
对象拷贝(直接使用赋值号) :
- 默认情况下,这种类对象的拷贝,是将每个成员变量逐个拷贝。
- 在类中,定义适当的“赋值运算符==”,就能够控制对象的这种拷贝行为。
拷贝构造函数:
- 普通拷贝行为;
- 默认情况下,类对象的拷贝是每个成员变量逐个拷贝;
拷贝函数的种类:
- 如果类中没有定义拷贝构造函数,编译器将提供一个拷贝构造函数(只有一个参数,是所属类型的引用),作用:将已存在的对象的成员变量赋值给新对象的成员变量。
- 重载的拷贝构造函数,第一个参数是所属类型的引用,其他参数必须有默认值(函数默认参数必须放在函数声明中(除非没有函数声明))。
拷贝构造函数的作用:
Student stu1;
Student stu2 = stu1;
Student stu3(stu2);
在一定的时机(等号赋值初始化类对象时),被系统自动调用。
- 建议拷贝构造函数的第一个参数(所属的类类型的引用)带
const
。 - 拷贝构造函数,一般不要声明
explicit
。
调用时机:
class A
{
public:
A()
{
cout << "A()" << endl;
}
A(int m_data) : data(m_data)
{
cout << "A(int m_a)" << endl;
}
A(const A& a)
{
this->data = a.data;
cout << "A(const A& a)" << endl;
}
virtual ~A()
{
cout << "~A()" << endl;
}
private:
int data;
};
A test(A a)
{
A a_(a);
cout << "......" << endl;
return a_;
}
int main()
{
A a, a2(2);
a2 = test(a2);
return 0;
}
/*
A()
A(int m_a)
A(const A& a)
A(const A& a)
......
A(const A& a)
~A()
~A()
~A()
~A()
~A()
*/
- 当用一个对象去初始化同类的另一个对象时,会引发拷贝构造函数被调用。
- 作为形参的对象,是用拷贝构造函数初始化的,而且调用拷贝构造函数时的参数,就是调用函数时所给的实参。
- 作为函数以值传递方式返回的对象是用拷贝构造函数初始化的(拷贝构造函数时的实参,就是 return 语句所返回的对象)。
- vs中,函数以值方式返回对象时,会调用拷贝构造函数;
- 但g++编译器中做了优化,并不会销毁函数中的局部对象,在不调用拷贝构造函数的前提下,直接将它作为对象返回;
注意事项:
-
自己定义的“拷贝构造函数”,会代替“系统默认的逐个成员变量的拷贝”行为自己定义的拷贝构造函数,必须要在拷贝函数中给类成员赋值。
-
拷贝构造一定要const修饰,为了兼容处理两种场景(传入的被拷贝对象是const和非const类型)。
- 常量对象只能调用const修饰的成语函数。
- 如果拷贝构造函数不加const修饰,则无法进行“拷贝构造”,只能调用默认构造或有参构造。
-
如果没有自己定义拷贝构造函数,编译器会定义一个“合成拷贝构造函数”,其会将参数中的成员逐个拷贝到正在创建的对象中。
-
每个成员变量的类型决定了如何被拷贝:
如果成员变量是类类型,就会调用这个类的拷贝构造函数来拷贝。
拷贝赋值函数,即重载赋值=运算符的函数:
- 赋值构造函数中,需要避免对象自我赋值(在赋值构造函数内部,赋值前增加判断相关代码)。
- 如果一个类有const或引用成员,则不能使用合成的拷贝赋值操作。
对象移动、移动构造函数、移动赋值运算符:
对象移动,A移动到B:
- 只是内存空间的所有权发生了变化,没有发生拷贝操作;
- 对象A无法再使用了;
移动构造函数,作用:
源对象指向的内存,直接让临时对象指向这段内存,并打断源对象与这段内存的联系(完成所谓的内存移动)。
A(A&& a) // 移动构造函数,形参不能用const修饰,因最后要将a.m_data置空
{
cout << "A(const A&& a)" << endl;
if (m_data != nullptr) // 如果已分配内存,则先释放掉
{
delete m_data;
}
m_data = a.m_data; // 将源对象中的指针指向的内存地址,赋值给新对象中的指针
a.m_data = nullptr; // 将源对象中的指针置空
}
移动赋值运算符,作用:
- 干掉该对象自己的内存;
- 源对象指向的内存,直接让自己的对象指向这段内存;
- 打断源对象与这段内存的联系(完成所谓的内存移动);
A& operator=(A&& a)
{
cout << "A&& A(A&& a)" << endl;
if (this == &a) // 避免“自我赋值”
{
return *this;
}
if (m_data != nullptr) // 如果已分配内存,则先释放掉
{
delete m_data;
}
m_data = a.m_data; // 将源对象中的指针指向的内存地址,赋值给新对象中的指针
a.m_data = nullptr; // 将源对象中的指针置空
return *this;
}
注意:避免“自我赋值”。
合成的移动操作(某些条件下,编译器能合成移动赋值运算符):
条件:只有一个类没有定义任何拷贝成员(拷贝构造函数和拷贝赋值运算符),且类的每个非静态成员都是可以移动的。
非静态成员可以移动的条件:
- 内置类型是可以移动的;
- 如果有类类型的成员,则这个类要有对应的移动操作相关的函数;
重载运算符:
本质是一个函数,即operator运算符(参数列表)
。
#include <iostream>
using namespace std;
class Complex
{
public:
friend ostream& operator<<(ostream& out, const Complex& complex);
friend istream& operator>>(istream& in, Complex& complex);
public:
Complex() // 默认构造函数
{
this->real = 0.0;
this->imag = 0.0;
cout << "Complex()\t" << this->real << "+" << this->imag << "i" << endl;
}
Complex(const Complex& complex) // 拷贝构造函数
{
real = complex.real;
imag = complex.imag;
cout << "Complex(const Complex& complex)\t" << this->real << "+" << this->imag << "i" << endl;
}
Complex(const double& m_real, const double& m_imag) // 有参构造函数
{
this->real = m_real;
this->imag = m_imag;
cout << "Complex(const double& m_real, const double& m_imag)\t" << this->real << "+" << this->imag << "i" << endl;
}
virtual ~Complex() // 虚析构函数
{
cout << "~Complex()\t" << this->real << "+" << this->imag << "i" << endl;
}
// 会产生并返回一个临时对象
Complex operator+(const Complex& complex) // 重载+运算符
{
//Complex tmp_complex;
//cout << "Complex operator+(const Complex& complex),重载+运算符 " << endl;
//tmp_complex.real = this->real + complex.real;
//tmp_complex.imag = this->imag + complex.imag;
//return tmp_complex; // 调用拷贝构造函数,将tmp_complex拷贝给临时对象
/* 。。。效率更高。。。 */
// 会直接调用有参构造函数,来产生一个临时对象
return Complex(this->real + complex.real, this->imag + complex.imag);
}
Complex& operator=(const Complex& complex) // 重载=赋值运算符
{
if (this != &complex)
{
this->real = complex.real;
this->imag = complex.imag;
}
return *this;
}
Complex& operator+=(const Complex& complex) // 重载+=赋值运算符
{
this->real += complex.real;
this->imag += complex.imag;
return *this;
}
bool operator==(const Complex& complex) // 重载==赋值运算符
{
return (this->real == complex.real) && (this->imag == complex.imag);
}
// 前置和后置操作符
Complex& operator++() // 前置++
{
this->real++;
this->imag++;
return *this;
}
Complex operator++(int) // 后置++
{
后置操作符内部会产生(通过拷贝构造函数产生)局部的对象,并之后调用拷贝构造函数产生临时对象
//Complex tmp_complex(*this);
//this->real++;
//this->imag++;
//return tmp_complex;
// 会直接调用有参构造函数,来产生一个临时对象
return Complex(this->real++, this->imag++);
}
private:
double real;
double imag;
};
// 重载左移运算符的函数必须是全局函数
ostream& operator<<(ostream& out, const Complex& complex)
{
out << complex.real << " + " << complex.imag << "i" << "\n";
return out;
}
// 重载右移运算符的函数也必须是全局函数
istream& operator>>(istream& in, Complex& complex)
{
in >> complex.real >> complex.imag;
return in;
}
Complex test()
{
// 通过默认构造函数,产生局部的对象
Complex complex;
// 调用拷贝构造函数,产生临时对象;之后析构掉局部对象;
return complex;
}
int main()
{
/* 四种自定义类型,对象的初始化方式 */
Complex complex1; // 调用默认构造函数
Complex complex2(1.0, 2.0); // 调用有参构造函数
Complex complex3(complex2); // 调用拷贝构造函数
Complex complex4 = complex1 + complex2; // 重载+运算符
cout << "。。。。。。。。。" << endl;
complex4 = complex1 + complex3 + complex3; // 重载+运算符、重载=赋值运算符
cout << "。。。。。。。。。" << endl;
Complex complex5 = complex4++;
complex5 = ++complex4;
cout << "。。。。。。。。。" << endl;
Complex complex6 = test();
cout << "。。。。。。。。。" << endl;
// 调用重载的<<运算符
cout << complex1 << complex2 << complex3 << complex4 << complex5 << complex6 << endl;
// 调用重载的>>运算符
cin >> complex6;
cout << complex1 << complex2 << complex3 << complex4 << complex5 << complex6 << endl;
return 0;
}
算术运算符重载(+、-、*、/):
void* operator+(...)
void* operator-(...)
void* operator*(...)
void* operator/(...)
一元运算符重载(++、–、!、&、~、*):
++
自增、--
自减、!
逻辑非、&
取址、~
二进制取反、*
解引用。
++
自增、--
自减,有前置和后置的区别:
/*
Complex& operator++() // 前置++
Complex operator++(int) // 后置++
*/
#include<iostream>
using namespace std;
class Point
{
public:
Point();
Point(double x, double y);
~Point();
// int型的形参,没有什么实际作用,只是用来区别前置自增运算符的重载函数
Point& operator++(); //前置
Point operator++(int); //后置
Point& operator--(); //前置
Point operator--(int); //后置
Point operator+(const Point &p)const;
void display() const;
private:
double x;
double y;
};
Point::Point()
{
this->x = 0; this->y = 0;
}
Point::Point(double x, double y)
{
this->x = x; this->y = y;
}
Point::~Point()
{
// cout << "析构函数" << endl;
}
Point& Point::operator++()
{
this->x++; this->y++;
return *this;
}
// 先将当前对象拷贝一份,再对当前对象进行加一操作,返回的是之前的对象
Point Point::operator++(int)
{
Point oldpoint = *this;
++(*this); // 这里调用的是重载的前++运算符
return oldpoint;
}
Point& Point::operator--()
{
this->x--; this->y--;
return *this;
}
Point Point::operator--(int)
{
Point oldpoint = *this;
--(*this); // 这里调用的是重载的前--运算符
return oldpoint;
}
Point Point::operator+(const Point &p) const
{
// 创建一个临时无名对象,并返回给调用者
return Point(this->x + p.x, this->y + p.y);
}
void Point::display() const
{
cout << "(" << this->x << "," << this->y << ")" << endl;
}
重载<<运算符:
/* void* operator<<(...) */
#include <iostream>
using namespace std; // 指定缺省的命名空间
class Employee
{
private:
string name;
int age;
int salary;
public:
Employee() : name(""), age(0), salary(0)
{
cout << "default constructor" << endl;
}
Employee(string m_name, int m_age, int m_salary) : name(m_name), age(m_age), salary(m_salary)
{
cout << "with the constructor" << endl;
}
~Employee()
{
cout << "destructor" << endl;
}
friend ostream& operator<<(ostream& out, const Employee& employee);
};
ostream& operator<<(ostream& out, const Employee& employee)
{
out << employee.name << ", " << employee.age << ", " << employee.salary;
return out;
}
int main()
{
Employee employee1("wang", 26, 10000);
Employee employee2("li", 26, 10000);
cout << employee1 << "\n";
cout << employee2 << endl;
return 0;
}
注意:<<
只能通过全局函数重载。
重载关系运算符(==、!=、>、>=、<、<=):
#include <iostream>
using namespace std; // 指定缺省的命名空间
class Employee
{
private:
string name;
int age;
int salary;
public:
Employee() : name(""), age(0), salary(0)
{
cout << "default constructor" << endl;
}
Employee(string m_name, int m_age, int m_salary) : name(m_name), age(m_age), salary(m_salary)
{
cout << "with the constructor" << endl;
}
~Employee()
{
cout << "destructor" << endl;
}
bool operator==(const Employee& employee)
{
return this->salary == employee.salary;
}
int operator+(const Employee& employee)
{
return this->salary + employee.salary;
}
bool operator<(const Employee& employee)
{
return this->salary < employee.salary;
}
bool operator>(const Employee& employee)
{
return this->salary > employee.salary;
}
};
int main()
{
Employee employee1("wang", 26, 10000);
Employee employee2("wang", 26, 10000);
cout << "whether salary is same : " << (employee1 == employee2) << endl;
cout << "whether employee1's salary is lower : " << (employee1 < employee2) << endl;
cout << "the total salary of employee1 and employee2 is : " << (employee1 + employee2) << endl;
return 0;
}
注意:建议使用成员函数版本。
重载new&delete运算符:
c++中的new/delete背后操作:
c++中的new做了两件事情:
- 调用标准库函数
operator new()
分配内存; - 调用构造函数初始化内存;
c++中的delete做了两件事情:
- 调用析构函数;
- 调用标准库函数
operator delete()
释放内存;
重载内存分配/释放new/delete函数:
void* operator new(size_t size);
void operator delete(void* ptr);
#include <iostream>
using namespace std;
class Stu
{
public:
string name;
int age;
public:
Stu() : name(""), age(0)
{
cout << "default constructor" << endl;
}
Stu(string m_name, int m_age) : name(m_name), age(m_age)
{
cout << "with the constructor" << endl;
}
void* operator new(size_t size)
{
cout << "调用重载的new运算符" << endl;
void* ptr = malloc(size);
cout << "申请的内存地址:" << ptr << endl;
return ptr;
}
void operator delete(void* ptr)
{
cout << "调用重载的delete运算符" << endl;
if (ptr == nullptr)
{
return;
}
free(ptr);
}
~Stu()
{
cout << "destructor" << endl;
}
};
int main()
{
Stu* stu = new Stu; // 先调用重载的运算符new,再调用默认构造函数初始化
delete stu; // 先调用析构函数,再调用重载的delete运算符
return 0;
}
注意:重载内存分配/释放new/delete函数,可以是全局函数或成员函数。
- 一个重载了new和delete运算符的类,尽管不显示指定使用static,实际上仍是静态成员函数。
- 重载new和delete运算符的函数中,不能调用类中的非静态成员变量。
重载new[]/delete[]运算符:
重载=、[]、()、->运算符:
注意:重载=、[]、()、->运算符,只能通过成员函数进行重载。
重载赋值=运算符:
类名& operator=(const 类名& 源对象)
#include <iostream>
#include <cstring>
using namespace std; // 指定缺省的命名空间
const int friends_size = 10;
class Employee
{
public:
string name;
int age;
string* friends; // 动态分配内存空间
public:
Employee() : name(""), age(0), friends(nullptr)
{
cout << "constructor" << endl;
}
Employee(string m_name, int m_age, string* m_friends) : name(m_name), age(m_age)
{
this->friends = new string[friends_size];
for (int i = 0; i < friends_size; ++i)
{
this->friends[i] = m_friends[i];
}
cout << "with the constructor" << endl;
}
~Employee()
{
if (this->friends != nullptr)
{
delete[] this->friends;
this->friends = nullptr;
}
cout << "destructor" << endl;
}
// 重载赋值函数,进行“深拷贝”
Employee& operator=(const Employee& employee)
{
if (this == &employee) // 避免“自我赋值”
{
return *this;
}
if (employee.friends == nullptr)
{
if (this->friends != nullptr)
{
delete[] this->friends;
this->friends = nullptr;
}
}
else
{
if (this->friends == nullptr)
{
this->friends = new string[friends_size];
}
for (int i = 0; i < friends_size; ++i)
{
this->friends[i] = employee.friends[i];
}
}
this->age = employee.age;
this->name = employee.name;
return *this;
}
string& operator[](int idx)
{
if (idx > friends_size)
{
cout << "out of range" << endl;
}
return this->friends[idx];
}
};
int main()
{
string friends[10] = {"yu", "sii", "yoyo", "dfdf", "", "", "", "", "", ""};
Employee employee1("wang", 26, friends);
Employee employee2;
employee2 = employee1;
for (int i = 0; i < friends_size; ++i)
{
cout << employee2[i] << ",";
}
cout << endl;
return 0;
}
-
如果类中重载了赋值函数,编译器则不再提供,否则编译器会默认提供一个实现成员变量“浅拷贝”的函数。
如果对象中不存在堆区内存空间,默认赋值函数即可满足条件,否则需要“深拷贝”。
-
重载拷贝赋值运算符,需要检测自我赋值
self assignment
,避免出错。 -
重载赋值函数 和 拷贝构造函数的区别:
赋值运算是指已经存在的两个对象,其中一个给另一个赋值;
拷贝构造函数是指用已存在的对象,给不存在的对象进行构造;
重载下标[]运算符:
1)返回值类型& operator[](参数列表) // 既能访问元素,又能修改元素的值
2)const 返回值类型& operator[](参数列表) const // 只能访问元素
#include <iostream>
using namespace std; // 指定缺省的命名空间
class Employee
{
private:
string name;
int age;
string friends[10];
public:
Employee(string m_name, int m_age, string m_friends[10]) : name(m_name), age(m_age)
{
for (int i = 0; i < 10; ++i)
{
this->friends[i] = m_friends[i];
}
cout << "with the constructor" << endl;
}
~Employee()
{
cout << "destructor" << endl;
}
// 两种方式可以同时存在(只会调用第一种):要防止数组下标越界!!!
string& operator[](int idx)
{
return friends[idx];
}
// 存在的目的:让const对象能够正常的通过调用const成员函数,来调用重载的operator[]运算符
const string& operator[](int idx) const
{
return friends[idx];
}
};
int main()
{
string friends[10] = {"yu", "sii", "yoyo", "dfdf", "", "", "", "", "", ""};
Employee employee1("wang", 26, friends);
employee1[0] = "wu";
cout << employee1[0] << endl;
return 0;
}
重载圆括号()运算符:
// 目的:将对象名作为函数使用,又称函数对象/仿函数
void operator()(...)
// 如果函数对象名与全局函数同名,则需要按作用域规则选择调用的函数。
/*
用途:
1)STL中,将用其作为可调用对象代替函数;
2)函数对象的本质是类,可以用成员变量存放更多的信息;
*/
#include <iostream>
using namespace std;
class Stu
{
public:
string name;
int age;
public:
Stu() : name(""), age(0)
{
cout << "default constructor" << endl;
}
Stu(string m_name, int m_age) : name(m_name), age(m_age)
{
cout << "with the constructor" << endl;
}
void operator()(string m_name, int m_age)
{
name = m_name;
age = m_age;
cout << "调用重载()运算符函数" << endl;
}
~Stu()
{
cout << "destructor" << endl;
}
};
void stu(string m_name, int m_age)
{
cout << "全局函数" << endl;
}
int main()
{
Stu stu("yoyo", 12); // 调用有参构造函数
Stu("yoyo", 12); // 调用有参构造函数
stu("wowo", 12); // 调用重载()运算符的函数
// 全局函数与类对象名同名
::stu("wowo", 12); // 调用本命名空间的全局函数
return 0;
}
重载->运算符:
void* operator->(...)
不能重载的5个运算符(五大创世宝石):
. // 成员引用运算符
.* // 成员指针引用运算符
sizeof // 运算符
?:: // 唯一的三目运算符
:: // 作用域操作符
细节问题:
-
重载运算符既然是一个函数,就会有返回类型和参数列表:
-
参数是运算符的运算对象,参数列表的顺序决定了操作数的位置。
class Stu { private: string name; int age; public: Stu() : name(""), age(0) {} friend Stu& operator+(Stu& stu, int val); friend Stu& operator+(int valStu, & stu); friend Stu& operator+(Stu& stu1, Stu& stu2); };
-
参数列表中至少有一个自定义类型,防止为内置数据类型重载运算符。
-
-
运算符重载函数返回值类型要与运算符本身的含义一致。
-
重载运算符函数:
- 全局函数版本:形参个数与运算符的操作数个数相同。
- 成员函数版本:形参个数比运算符操作数的个数少一个(少的那个操作数,隐式传递了调用对象)。
注意:
- 同时,只能有重载非成员函数和成员函数版本中的一个,否则会出现二义性。
- 如果重载运算符既可以是成员函数也可以是全局函数,应该优先考虑成员函数。
*函数调用运算符:
圆括号()就是函数调用最明显的标志,称为“函数调用运算符”。
class T
{
public:
T(int val)
{
cout << "调用了类T的有参构造函数" << endl;
}
void operator()(int val)
{
cout << "调用了类T的重载()运算符" << endl;
}
};
void test()
{
T t(3); // 调用了类T的有参构造函数
t(3); // 调用了重载运算符,等价于a.operator()(3);
}
-
如果类中重载了函数调用运算符(),那么我们需要先定义一个类对象,之后就可以像使用函数一样使用该类的对象了。
注:类中允许有多个版本的重载()运算符的出现。
-
不同的对象(类对象、函数),如果调用参数和返回值相同,就称为有“相同的调用形式”。一种调用形式,对应一种函数类型,例如int(int)。
析构函数:
格式:~函数名()
,没有返回值和参数列表(不能被重载)。
- 一个给定的类,可以有多个构造函数,但只能有一个析构函数,即构造函数可以重载但析构函数不可以。
对象在销毁的时候,自动调用析构函数。
构造函数和析构函数的作用:
- 构造函数干了两件事,函数之前和函数体中。
- 析构函数也干了两件事,函数体之中和函数体之后 。
基类的析构函数,一般是虚析构函数,即使它不使用析构函数也应提供一个空虚析构函数。
类型转换:
类型转换构造函数:
可以将某个其他数据类型转换为该类类型的对象。
//显示的类型转换运算符explicit:禁止隐式类型转换,只能进行显示类型转换
//explicit TestInt(int x) :m_valueX(x)
TestInt(int x) :m_valueX(x)
{
if (m_valueX < 0)
{
m_valueX = 0;
}
else if (m_valueX > 100)
{
m_valueX = 100;
}
cout << "调用了类TestInt的类型转换构造函数" << endl;
}
void TypeConversionConstructor()
{
// 编译器将12这个数字,通过调用TestInt类的类型构造函数来创建一个临时的TestInt对象,并把这个对象构造到t2的预留空间里去了
//TestInt t1 = 12; // 隐式类型转换,将数字12转换为TestInt对象(调用类型转换构造函数)
TestInt t2 = TestInt(12); // 调用显示类型转换构造函数
TestInt t3(12); // 调用类型转换构造函数,并进行了显式类型转换
}
-
只有一个参数时,该参数是待转换的数据类型(不是本类的const引用);
-
在类型转换构造函数中,我们要指定转换方法;
-
显示的类型转换运算符
explicit
:禁止隐式类型转换,只能进行显式类型转换;注意:实际开发中,如果强调的是构造,建议使用explicit;如果强调的是类型转换,建议不使用explicit;
类型转换运算符(与类型转换构造函数正好相反):
// 类型转换运算符(是特殊的成员函数):与类型转换构造函数正好相反,能够将一个类类型对象 转换为 某个其他数据类型
operator int() const
{
cout << "调用了类TestInt的类型转换运算符(将本类对象转换为int类型)" << endl;
return m_valueX;
}
void TypeConversionOperator()
{
TestInt t4;
t4 = 12;
int k = t4 + 5; // 隐式调用operator int() const,将t4转换成了int;再进行加法运算
cout << k << endl;
int k2 = static_cast<int>(t4) + 5; // 隐式调用operator int() const,将t4转换成了int;再进行加法运算
cout << k2 << endl;
int k3 = t4.operator int() + 5; // 显式调用operator int(),将t4转换成了int;再进行加法运算
cout << k3 << endl;
}
最特殊运算符成员函数,能够将一个类类型对象 转换为 某个其他数据类型。
- 格式:
operator type() const
; - const是可选项,表示一般不应该改变待转换对象的内容;
- type:表示要转换的目标类型(数组指针、函数指针、引用等类型);
- 类型转换运算符,形参列表为空(因为类型转换运算符是隐式执行的,故无法给它传递参数) ,也不能指定返回类型但能返回一个指定type类型的值;
- 必须定义为类的成员函数;
c++11后,可使用explicit operator type() const
,表示必须显式的调用类型转换运算符,且函数内不能修改类对象的成员变量。
注意:实际开发中不建议使用。
类对象转换为函数指针:
class TestInt2
{
// 两种方式:定义一个函数指针类型,代表的函数带一个int形参,没有返回类型
typedef void(*tfPtr)(int);
//using tfPtr = void(*)(int);
public:
// 将类对象转换为函数指针
static void myStaticFunc(int x)
{
cout << "调用了类TestInt2的静态成员函数" << endl;
}
// 新的类型转换运算符,将本类类型对象 转换为 一个函数指针类型
operator tfPtr() // const不是必须加的
{
cout << "调用了类TestInt2的类型转换运算符(将本类类型对象转换为函数指针类型)" << endl;
return myStaticFunc; // 函数地址(函数名),作为函数指针类型返回即可
}
virtual ~TestInt2()
{
cout << "调用了类TestInt2的虚析构函数" << endl;
}
};
void ClassObject_FunctionPointer()
{
TestInt2 testInt;
// 相当于调用了两个函数:类型转换运算符(转换成函数指针类型);通过函数指针调用具体的函数
(testInt.operator TestInt2::tfPtr())(12);
}
类型转换的二义性问题:
- 一个类中,尽量只出现一个类型转换运算符;
- 减少使用隐式类型转换函数,通常是显式地调用类型转换运算符;
static成员:
类名::静态成员变量; //推荐
对象.静态成员变量; //和上面等价
类名::静态成员函数(实参表); //推荐
对象.静态成员函数(实参表); //和上面等价
定义静态成员变量(分配内存):
- 一般会在某个.cpp源文件开头,来定义这个静态成员变量。这样保证了在调用任何函数之前,这个静态成员变量已经被初始化。
- 定义的格式:
类名::静态变量名 = ....
;
静态变量(保存在静态存储区):
- 局部静态变量
- 全局静态变量:限制该成员函数只能在本文件中使用。
静态成员变量:
在类中,声明一个静态成员变量,并未分配内存,无法正常使用。
- 为了能够使用,必须在类外定义静态成员变量(即分配内存空间)
- const静态成员,可以在类内初始化。
特点:静态成员变量属于类,不属于对象。
- 一旦在某个类对象中,修改了这个成员变量的值,其他对象直接能够看到被修改的结果。
- 静态成员变量在类中只有一份,且生命周期与程序运行周期相同,属于类(即使不创建对象也能访问静态成员变量)。
- 静态成员变量和全局变量类似,不占用对象的内存,存储在全局区,可以把静态成员变量理解成被限制在类中使用的全局变量。
可以通过 类名或对象名,来引用静态成员变量。
静态成员函数:
-
静态成员函数类似于静态成员变量,都属于类而不是对象。
-
静态成员函数,仅可以调用类的静态成员变量,不可以调用普通成员变量。
-
静态成员函数不具有this指针,故不能用const修饰静态成员函数。
class A { public: A(){} int val(); // static int stval ()const; 出错,不具有this指针 private: const static int bc=2;//常量静态成员可以在类内初始化 };
非静态成员函数中,可以访问静态成员:
私有的静态成员,在类外无法访问:
类成员指针:
class CMPtr
{
public:
CMPtr()
{
cout << "调用CMPtr的默认构造函数" << endl;
}
void originalFunc(int tmpValue)
{
cout << "调用了originalFunc普通成员函数, value = " << tmpValue << endl;
}
// 如果类中有虚函数,则编译器会给该类生成虚函数表
virtual void virtualFunc(int tmpValue)
{
cout << "调用了virtualFunc虚成员函数, value = " << tmpValue << endl;
}
static void staticFunc(int tmpValue)
{
cout << "调用了staticFunc静态成员函数, value = " << tmpValue << endl;
}
virtual ~CMPtr()
{
cout << "调用CMPtr的虚析构函数" << endl;
}
public:
int m_value;
static int m_staticValue; // 静态成员变量属于类(不属于对象)
};
类成员变量指针:是指向类成员变量的指针
定义:类型 类名::*成员指针变量名 = &类名::成员变量;
void ClassMemberVariablePointer()
{
// 类成员变量指针
// 1、对于普通类成员变量
int CMPtr::*myMVPtr; // 等价于int CMPtr::*myMVPtr = &CMPtr::m_value;
myMVPtr = &CMPtr::m_value; // 0x00000004,并不是真正意义上的内存地址,而是该成员变量与该类对象指针的偏移量
CMPtr cmPtr;
// 当生成对象时,如果这个类中有虚函数表,则对象中就会有一个指向这个虚函数表的指针(这个指针占用4个字节)
// 因此,该成员变量与该类对象指针的偏移量是4,而不是0
cmPtr.*myMVPtr = 12; // 通过类成员变量指针,来修改成员变量值,等价于cmPtr.m_value = 12;
// 2、对于静态类成员变量
// 这种指向静态成员变量的指针,是有真正的内存地址的(而不是只有偏移量)
int *myMSPtr; // 等价于int *myMSPtr = &CMPtr::m_staticValue;
//myMSPtr = &CMPtr::m_staticValue;
//*myMSPtr = 12;
}
两种使用方法:
- `对象.*成员变量指针名;``
- ``对象指针->*成员变量指针名;`
普通指针和成员变量指针的区别:
- 普通指针:
int *ptr = &cmPtr.m_name;
- 成员变量指针:
int CMPtr::*myMVPtr = &CMPtr::m_value;
成员变量指针的本质:是类中特定成员在对象中的相对地址。
类成员函数指针:是指向类成员函数的指针
定义:返回类型 (类名::*成员函数指针)(形参表) = &类名::成员函数名;
void ClassMemberFunctionPointer()
{
// 类成员函数指针:是指针,指向类成员函数
// 1、定义一个普通的类成员函数指针
// 格式:返回类型 (类名::*函数指针变量名)(形参列表) ,声明普通成员函数指针
// &类名::成员函数名 ,获取类成员函数地址(真正的内存地址)
void(CMPtr::*myOriginalFPtr)(int); // 定义一个变量名为myFPtr的类成员函数指针变量
myOriginalFPtr = &CMPtr::originalFunc; // 类成员函数指针变量myFPtr被赋值
// 注意:成员函数是属于类的(不属于类对象),只要有类在就有成员函数的地址
// 但若要使用该成员函数指针,就必须把它绑定到一个类对象上,才能调用
// 使用函数指针的格式: "类对象名.*函数指针变量名"来调用,如果是对象指针,则调用格式"指针名->*函数指针变量名"
CMPtr cmPtr, *myCMPtr;
myCMPtr = &cmPtr;
(cmPtr.*myOriginalFPtr)(100);
(myCMPtr->*myOriginalFPtr)(200);
// 2、定义虚成员函数的类成员函数指针并赋值(与普通函数写法相同)
void(CMPtr::*myVirtualFPtr)(int);
myVirtualFPtr = &CMPtr::virtualFunc; // “真正的内存地址”,不是虚函数表的偏移量
//也必须绑定到类对象上,才能调用
(cmPtr.*myVirtualFPtr)(100);
(myCMPtr->*myVirtualFPtr)(200);
// 3、定义静态成员函数的类成员函数指针并赋值
void(*myStaticFPtr)(int);
myStaticFPtr = &CMPtr::staticFunc; // “真正的内存地址”,不是虚函数表的偏移量
myStaticFPtr(100); // 因为静态成员是属于类的,故可以直接使用静态成员函数指针名即可调用
}
两种使用方法:
(对象.*成员函数指针)(实参表);
(对象->*成员函数指针)(实参表);
注意:
-
成员函数是属于类的(不属于类对象),只要有类在,就有成员函数的地址。
-
若要使用该成员函数指针,就必须把它绑定到一个类对象上(除了静态成员函数),才能调用。
-
const修饰的成员函数,创建函数指针时必须在声明后加上const,否则会报“类型不匹配”的错误。、
#include <iostream> using namespace std; class A { public: void strpcy(char*, const char*) {} void strcat(char*, const char*) {} void touppercase(char*, const char*) const {} }; int main() { A a; char dest[6]; const char* src = "hello"; void(A::*pmf)(char*, const char*); pmf = &A::strcat; // pmf是类A成员函数指针变量 (a.*pmf)(dest, src); //pmf = &A::touppercase; // 出错,类型不匹配 // Error: cannot convert ‘void (A::*)(char*, const char*) const’ to ‘void (A::*)(char*, const char*)’ // 解决的方法:声明一个const类型的成员函数指针变量: void(A::*pcmf)(char*, const char*) const; pcmf = &A::touppercase; (a.*pcmf)(dest, src); return 0; }
-
因为静态成员是属于类的,故可以直接使用静态成员函数指针名即可调用。
友元关系:
在函数/类声明前加friend,友元全局函数、友元类、友元成员函数。
友元全局函数:
在有友元全局函数中,可以访问某个类中的所有成员(包括public、protected、private)。
class Stu
{
private:
string name;
int age;
public:
// 友元全局函数
friend ostream& operator<<(ostream& out, const Stu& stu);
};
ostream& operator<<(ostream& out, const Stu& stu)
{
out << stu.name << "," << stu.age << endl;
return out;
}
友元类:
声明其他类为本类的友元类,那么它就能够访问本类中的所有成员。
class Stu
{
private:
string name;
public:
// 友元类
friend class People;
};
class People
{
public:
void show(const Stu& stu)
{
cout << stu.name << endl;
}
}
注意:设置一个类为友元类,会破坏封装。
友元成员函数:
通过声明某个类的成员函数为本类的友元函数,它就能够访问本类中的所有成员。
优/缺点:
- 优点:允许特定情况下,某些非成员函数访问类的protected、private成员,从而提出友元的概念,使得访问protected、private成员更灵活。
- 缺点:破坏类的封装性;降低了类的可靠性和可维护性。
特点:
-
友元函数不含有this指针。
-
友元函数可以直接调用。
-
友元关系不能被继承。
#include <iostream> using namespace std; class A { friend class B; private: A() { cout << "A::A()" << endl; } int m_a; }; class B : public A { public: int m_b; B() : A(), m_b(0) { cout << "B::B()" << endl; } }; class C : public B { public: int m_c; C() : B(), m_c(0) { cout << "C::C()" << endl; } }; int main() { C c; // 报错,由于友元关系(B是A的友元类)不能被继承(C继承了B,但并不能继承B对A的友元关系) //,则在C类的类对象不能调用A类的私有成员函数/变量 //cout << c.m_a << endl; return 0; }
-
友元关系是单向的,不具备交换性:A类是B的友元类,但B类不一定是A类的友元类。
-
友元关系不具备传递性。
内部类和友元类的区别:
友元类:内部友元类可以通过外部类的对象参数,来访问外部类中的所有成员。
class Stu
{
private:
string name;
public:
// 友元类
friend class People;
};
class People
{
public:
void show(const Stu& stu)
{
cout << stu.name << endl;
}
}
内部类:可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名。
class A
{
private:
static int k;
int h;
public:
class B
{
void foo(A a)
{
cout<< k <<endl;//OK
cout<< a.h<<endl;//OK
}
// 要使用另一个类的成员,则必须要存在该类的对象
void foo()
{
cout<< k <<endl; // OK,k属于静态成员属于整个类,不需要外部类的对象就已存在
cout<< a.h<<endl; // error,因内部类与外部类是独立的两个类,故还没有外部类的对象,显然也不存在h
}
};
};
int A::k=3;
注意:内部类和友元类很像,只是内部类比友元类多了一点访问权限,其他都一样。
不可继承的类:
法一:c++11中,final
关键字。
法二:友元类+虚继承。
#include <iostream>
using namespace std;
class A
{
friend class B;
private:
A()
{
cout << "A::A()" << endl;
}
int m_a;
};
class B : virtual public A
{
public:
int m_b;
B() : A(), m_b(0)
{
cout << "B::B()" << endl;
}
};
class C : public B
{
public:
int m_c;
C() : B(), m_c(0)
{
cout << "C::C()" << endl;
}
};
int main()
{
B b;
//C c;
// 报错,由于友元关系(B是A的友元类)不能被继承(C继承了B,但并不能继承B对A的友元关系)
//,则在C类的类对象不能调用A类的私有成员函数/变量,故C类并不能调用A类的构造函数,则无法产生C类对象
//cout << c.m_a << endl;
return 0;
}
/*
C 在调用构造函数时,会直接调用 A 的构造函数,
C不是 A 的友元类(C 不能继承 B 对 A 的友元特性),所以无法访问即无法初始化 A,最终 C 就不能继承 B。
利用 “友元不能被继承” 的特性。
*/
虚继承,解决二义性的问题?在虚派生中,由最低层次的派生类的构造函数初始化虚基类。
法三:私有化构造函数+静态公有方法访问构造函数。
#include <iostream>
using namespace std;
// 该方式创建不能被继承的类,方法类似于“单例模式”。
// 存在的问题:该类只能在堆区创建对象,栈区无法创建对象。
class Base
{
public:
static Base* Construct(int m_base) // 由于外界无法调用(默认、有参)构造函数,故不能通过传入Base&来实现对象构造
{
cout << "static Base* Construct(int m_base)" << endl;
Base* basePtr = new Base(m_base); // 不能在栈区创建的主要原因:内部定义的局部变量在函数栈帧结束后,会销毁
//,只有通过指针创建的对象,在函数结束后不销毁对象所在的堆区内存空间
//,通过函数传递出去的是对象内存所在的堆区地址
return basePtr;
}
static void Destruct(Base* basePtr)
{
cout << "static void Destruct(Base* basePtr)" << endl;
delete basePtr;
basePtr = nullptr;
}
int _m_base;
private:
Base() {}
Base(int m_base) : _m_base(m_base) {}
~Base() {}
};
int main()
{
Base* base = Base::Construct(10);
cout << base->_m_base << endl;
Base::Destruct(base);
return 0;
}
基类、派生类:
使用继承的场景:
- 创建的多个类中,有很多相似的成员变量和成员函数,可将这些类共同的成员提取出来,定义为基类,并继承它。
- 当新建类与现有类相似时,只是多出若干成员变量/函数时,可使用继承。
继承(面向对象的核心):基类和派生类
继承方式(访问权限):
- public:可以被任意实体访问;
- protected:只允许本类或者子类的成员函数来访问;
- private:只允许本类的成员函数来访问;
继承关系会一直传递,构成一种继承链,即最终的派生类包含了直接基类和间接基类的成员。
使用细节:
- using关键字,能够用于改变基类成员在派生类中的访问权限,一般不使用。
- 在可使用非public继承时,就不要再使用public继承。
public继承,举例:
class A {};
class B : public A {};
class C : public B {};
void func(A a)
{
cout << "A" << endl;
}
/*
func(b) ==> A
func(c) ==> A
*/
void func(B b)
{
cout << "B" << endl;
}
/*
func(b) ==> B
func(c) ==> B
*/
void func(C c)
{
cout << "C" << endl;
}
/*
func(c) ==> C
*/
成员遮蔽(成员函数遮蔽):
如果派生类中的成员(包括成员函数和成员变量)和基类中的成员重名,通过派生类对象或者在派生类的成员函数中使用该成员时,将使用派生类中新增的成员,而非基类的。
成员函数遮蔽:基类与派生类成员函数不会构成重载,派生类会遮蔽基类中所有的同名成员函数。
-
在子类的成员函数中,用“父类::函数名”,强制调用父类函数
-
当基类继承了多个父类时,通过增加作用域,可以明确的告诉系统调用的是基类1、还是基类2的成员函数
格式:
c3::C2.func();
或者c3.C1::func();
-
使用using关键字,也可以让父类同名函数在子类中可见,即让父类的同名函数以重载的形式来使用。
格式:
using 父类::函数名
,不能使用带参数的函数名。using的主要目的:在子类中调用父类同名函数的重载版本。函数名相同,但参数列表(参数个数、参数类型)不同。
如果一个类从它的基类中,继承了相同的构造函数,这个类必须为该构造函数定义自己的版本。
注意:public继承中,不建议 “子类遮蔽父类的普通成员函数”(既然父类将其作为普通成员函数,就代表子类不会对其做出不同行为),如果需要覆盖则将父类的该函数修改为虚函数。
子类调用父类同名函数:
#include<iostream>
using namespace std;
class Base
{
public:
void func1()
{
cout << "Base::func1()" << endl;
}
virtual void func2()
{
cout << "Base::func2()" << endl;
}
};
class Derive : public Base
{
public:
virtual void func2()
{
cout << "Derive::func2()" << endl;
}
};
int main()
{
Base b1;
b1.func1();
b1.func2();
Base* b2 = new Base;
b2->func1();
b2->func2();
Base* b3 = new Derive;
b3->func1();
b3->func2();
b3->Base::func2();
}
函数声明时,有virtual关键字:
-
子类和父类返回值、参数相同、函数名都相同,有virtual关键字,则由对象的类型决定调用哪个函数。
-
子类和父类参数不同、函数名相同,有virtual关键字,则不存在多态性,子类的对象没有办法调用到父类的同名函数(父类的同名函数被隐藏了)。
可以强制调用父类的同名函数
class::funtion_name
。 -
子类和父类返回值不同、参数相同、函数名相同,有virtual关键字,则编译出错error C2555编译器不允许函数名参数相同返回值不同的函数重载。
函数声明时,没有virtual关键字:子类和父类只要函数名相同,没有virtual关键字,则子类的对象没有办法调用到父类的同名函数(父类的同名函数被隐藏了)
- 可以强制调用父类的同名函数
class::funtion_name
。 - 也如果在子类的定义中,使用using即可将父类的同名函数暴露,然后可直接调用。
使用细节:
- 类可以定义父类的同名成员,子类隐藏父类同名成员;父类中的同名成员依然存在于子类中,用作用域分辨符
::
可以访问父类同名成员; - 子类和父类的函数不构成重载关系;子类可以重写父类成员函数;
- 子类对象可以当作父类对象使用(赋值兼容);
- 父类指针、引用可以指向子类对象(子类对象被退化为父类对象),能访问父类中的成员和被子类覆盖的同名成员;
派生类对象模型:
包含多个组成部分:
-
该派生类所继承的基类中定义的成员。
基类,既能够独立存在,也能够作为派生类对象的一部分存在。
-
该派生类定义的自己的成员。
-
通过
sizeof
查看,得到的是基类所有成员(包括私有成员)+派生类对象所有成员的大小。#include <iostream> #include <cstring> using namespace std; class A { public: A() : m_a1(1), m_a2(1), m_a3(1) { cout << "default constructor of base class" << endl; } ~A() { cout << "destructor of base class" << endl; } void showA() { cout << m_a1 << "," << m_a2 << "," << m_a3 << endl; } protected: int m_a1; int m_a2; private: int m_a3; }; class B : public A { public: B() : m_b1(1), m_b2(1), m_b3(1) { cout << "default constructor of derived class" << endl; } ~B() { cout << "destructor of derived class" << endl; } void showB() { cout << m_b1 << "," << m_b2 << "," << m_b3 << endl; } protected: int m_b1; int m_b2; private: int m_b3; }; int main() { B* bPtr = new B; cout << bPtr << ":" << sizeof(B) << endl; bPtr->showA(); bPtr->showB(); // 用memset函数可以从内存中清空基类的私有成员 memset(bPtr, 0, sizeof(B)); bPtr->showA(); bPtr->showB(); // 用指针可以访问到基类中的私有成员 cout << "A::m_a3 = " << *((int*)bPtr + 3) << endl; *((int*)bPtr + 3) = 12; cout << "A::m_a3 = " << *((int*)bPtr + 3) << endl; bPtr->showA(); bPtr->showB(); return 0; }
- c++中,不同继承方式的访问权限只是语法的处理。
- 通过
memset(void* dest, int ch, size_t count)
,可以从内存角度清空基类的私有成员。 - 用指针可以访问/修改基类中的私有成员。
默认构造函数、拷贝构造函数、移动构造函数、赋值运算符,编译器按照合成规则自动合成(除非该派生类已经自定义过了)
基类和派生类的创建和销毁时机:
-
创建派生类对象时,先调用直接基类的构造函数,再调用派生类的构造函数。
-
销毁派生类对象时,先调用派生类的析构函数,再调用基类的析构函数。
-
注意:
1)如果没有在派生类构造函数的初始化列表中显式调用直接基类的构造函数,则会调用直接基类的默认构造函数。
2)如果手工调用派生类的析构函数,也会自动调用基类的析构函数,即先销毁派生类后自动销毁基类。
派生类对象初始化一个基类对象:
#include <iostream>
using namespace std;
class A
{
public:
A() : m_a1(0), m_a2(0)
{
cout << "default constructor of base class" << endl;
}
A(int a1, int a2) : m_a1(a1), m_a2(a2)
{
cout << "with the constructor of base class" << endl;
}
A(const A& a) : m_a1(a.m_a1), m_a2(a.m_a2)
{
cout << "copy constructor of base class" << endl;
}
~A()
{
cout << "destructor of base class" << endl;
}
void showA()
{
cout << m_a1 << "," << m_a2 << endl;
}
private:
int m_a1;
protected:
int m_a2;
};
class B : public A
{
public:
// 调用基类的默认构造函数
B() : A(), m_b1(0), m_b2(0)
{
cout << "default constructor of derived class" << endl;
}
// 调用基类的有参构造函数
B(int a1, int a2, int b1, int b2) : A(a1, a2), m_b1(b1), m_b2(b2)
{
cout << "with the constructor of derived class" << endl;
}
// 调用基类的拷贝构造函数
B(const A& a, int b1, int b2) : A(a), m_b1(b1), m_b2(b2)
{
cout << "copy constructor of derived class" << endl;
}
~B()
{
cout << "destructor of derived class" << endl;
}
void showB()
{
cout << m_b1 << "," << m_b2 << endl;
}
private:
int m_b1;
protected:
int m_b2;
};
int main()
{
B b(1,2,3,4);
A a = b;
a.showA();
return 0;
}
-
c++对 “指针和引用类型” 与 “赋给的类型匹配”,但这一规则对继承无效。
-
用派生类对象(中的基类部分)来定义基类对象,会导致基类的拷贝构造函数的执行。
-
通过重载运算符,可以实现用派生类对象(中的基类部分)来初始化基类对象。
-
细节问题:
1)只有派生类的基类部分会被拷贝或赋值,派生类的其余部分将被忽略(即基类只干基类自己的事情)。
2)函数的形参列表中含有基类,则可以将派生类对象作为实参传入函数。
派生类中构造基类:
#include <iostream>
using namespace std;
class A
{
public:
A() : m_a1(0), m_a2(0)
{
cout << "default constructor of base class" << endl;
}
A(int a1, int a2) : m_a1(a1), m_a2(a2)
{
cout << "with the constructor of base class" << endl;
}
A(const A& a) : m_a1(a.m_a1), m_a2(a.m_a2)
{
cout << "copy constructor of base class" << endl;
}
~A()
{
cout << "destructor of base class" << endl;
}
void showA()
{
cout << m_a1 << "," << m_a2 << endl;
}
private:
int m_a1;
protected:
int m_a2;
};
class B : public A
{
public:
// 调用基类的默认构造函数
B() : A(), m_b1(0), m_b2(0)
{
cout << "default constructor of derived class" << endl;
}
// 调用基类的有参构造函数
B(int a1, int a2, int b1, int b2) : A(a1, a2), m_b1(b1), m_b2(b2)
{
cout << "with the constructor of derived class" << endl;
}
// 调用基类的拷贝构造函数
B(const A& a, int b1, int b2) : A(a), m_b1(b1), m_b2(b2)
{
cout << "copy constructor of derived class" << endl;
}
~B()
{
cout << "destructor of derived class" << endl;
}
void showB()
{
cout << m_b1 << "," << m_b2 << endl;
}
private:
int m_b1;
protected:
int m_b2;
};
int main()
{
B b1;
b1.showA(); b1.showB();
B b2(1,2,3,4);
b2.showA(); b2.showB();
A a3(2,2);
B b3(a3,4,4);
b3.showA(); b3.showB();
return 0;
}
-
定义构造函数时,只需要对派生类中新增成员进行初始化,对继承来的基类成员的初始化只需使用“派生类的构造函数初始化列表”显式调用直接基类构造函数。
-
通过派生类创建对象时,必须要显式调用基类的构造函数,否则会直接调用编译器指定的基类的默认构造函数。
// 显示的使用C1的有参构造函数来初始化基类子对象 C2(int i, int j, int k) :C1(i), m_valueC3(k) // 隐式的使用C1的默认构造函数(不带参数的构造函数)来初始化基类子对象 C2(int i, int j, int k) : m_valueC3(k)
*基类指针、虚析构函数、多态对象模型、虚函数/纯虚函数:
基类指针:
可以指向派生类对象,即基类* ptr = new 派生类
,该指向派生类对象的基类指针,可以调用基类成员函数,但无法调用派生类成员函数。
- 因为派生类中含有基类的成分,故可以将派生类当作基类使用,故派生类指针/引用是可以绑定到基类对象这部分上。
- 编译器会帮助我们做隐式类型转换(派生类到基类的转换),这样在需要基类指针或者引用时,可以new一个派生类对象的指针或者引用。
虚析构函数(基类的析构函数,一般写成虚析构函数):
-
用基类指针new子类对象时,delete基类指针时,系统不会调用派生类的析构函数;只有将基类中的析构函数变为虚函数,才能正常的调用派生类的析构函数。
这样基类中析构函数的虚属性,也会继承给子类,故子类的析构函数也是虚函数。
-
如果一个类想做基类,必须将析构函数写为虚析构函数(这样在delete基类指针时,才能正常的调用基类和派生类的析构函数),即使它不使用析构函数也应提供一个空虚析构函数。
-
并不是要把所有类的析构函数都写成虚函数:
1)当类里面有虚函数的时候,编译器会给类添加一个虚函数表,里面来存放虚函数指针,这样就会增加类的存储空间。
2)只有当一个类被用来作为基类,并且指向子类对象的时候,才把析构函数写成虚函数。
多态性(虚函数专用):
体现在具有继承关系的基类与派生类之间,派生类重写(重定义)基类中的虚成员函数。
const double Pi = 3.14;
class Shape
{
public:
virtual double Area() const = 0;
void Display()
{
cout << Area() << endl;
}
};
class Rectangle() : public Shape
{
public:
Rectangle(const double& m_width, const double& m_height) : width(m_width), height(m_height)
{
}
virtual double Area()
{
return width * height;
}
private:
double width;
double height;
};
class Circle() : public Shape
{
public:
Circle(const double& m_radius) : radius(m_radius)
{
}
virtual double Area()
{
return Pi * radius * radius;
}
private:
double radius;
};
// 多态性的体现:
int main()
{
Rectangle rectangle(2.0,3.0);
Circle circle(2.0);
// 子类重写了父类的纯虚函数
// 父类指针指向了子类对象
Shape* shape[2] = {&rectangle, &circle};
shape[0]->Display();
shape[1]->Display();
}
- 通过基类的虚函数实现基本功能的接口定义;
- 通过派生类重定义虚函数,扩展功能、提升性能或实现个性化功能;
有了虚函数,当基类指针
-
指向基类对象时,使用的就是基类的同名同参成员函数。
-
指向派生类对象时,使用的就是派生类对象的同名同参成员函数,进而在函数体内就可以访问派生类对象的成员函数。
,基类指针表现出了多种形式,该现象称为“多态”。
基类引用/指针,都可以使用多态:
#include <iostream>
using namespace std;
class A
{
public:
virtual void Display()
{
cout << "A" << endl;
}
};
class B : public A
{
public:
virtual void Display()
{
cout << "B" << endl;
}
};
int main()
{
B b;
// 基类指针指向派生类对象,并调用派生类中重写了的基类成员函数
A* a1 = &b;
a1->Display();
// 基类引用派生类对象,并调用派生类中重写了的基类成员函数
A& a2 = b;
a2.Display();
return 0;
}
注意:
- 基类中,定义虚成员函数时,只能在声明时加virtual关键字,定义时不能加;
- 派生类中重定义虚函数时,“函数特征”要相同(即同名同参);
- 当在基类中定义了虚函数时,但派生类没有重写该虚函数,那么将使用基类的虚函数;
- 当派生类中重定义了虚函数时,如果要使用基类函数,可以加类名和域解析符;
类的普通成员函数和虚成员函数:
普通成员函数的地址是静态的,编译时已确定。
类对象模型中的虚函数表:
- 如果派生类没有重定义基类的虚函数,则派生类对象的虚函数表中,存放的是基类的虚函数名和入口地址;
- 如果派生类重定义了基类的虚函数,则派生类对象的虚函数表中,存放的是基类的未被重写的、已被重写的派生类的虚函数名和入口地址;
虚函数:
-
虚函数的声明与定义要求非常严格,只有在派生类中的虚成员函数与基类虚成员函数一模一样的时候(包括限定符)才会被认为是真正的虚函数。
-
调用虚函数执行的是**“动态绑定”**,即运行时才决定该指针对象绑定的是哪个子类(
父类对象指针 = new 子类
中的子类),即最后调用哪个类的同名同参虚函数。 -
虚函数的实现是由两个部分组成的:虚函数表、虚函数指针。
虚函数指针:
- 本质是一个指向函数的指针,指向用户所定义的虚函数,具体是在子类里的实现,当子类调用虚函数的时候,实际上是通过调用该虚函数指针从而找到接口。
- 虚函数指针是确实存在的数据类型,在一个被实例化的对象中,总是被存放在该对象的地址首位(目的是为了保证运行的快速性)。
- 与对象的成员不同,虚函数指针对外部是完全不可见的,除非在DEBUG模式下直接访问虚函数表的地址,进而找到虚函数的地址。
- 每一个虚函数都会对应一个虚函数指针,所以拥有虚函数的类的所有对象都会因虚函数产生额外内存开销,且会在一定程度上降低程序运行效率。
虚函数表:
-
虚函数会增加访问内存开销,因类中定义了虚函数,导致编译器会给该类对象增加虚函数表指针(其中存放虚函数指针)。
每个拥有虚函数指针的类所实例化的对象,都会拥有该类中所有虚函数指针并且按照一定的顺序排列在对象的地址首部(这由编译器来保证,为了能高效的取到虚函数表),从而构成了一种表状结构,称为虚函数表
virtual table
。意味着,可以通过对象实例的地址得到这张虚函数表,然后就可以遍历到其中函数指针,并调用相应的函数。
class Base { public: virtual void f() { cout << "Base::f" << endl; } virtual void g() { cout << "Base::g" << endl; } virtual void h() { cout << "Base::h" << endl; } }; in main() { // 定义函数指针 typedef void(*Fun)(void); Base b; Fun pFun = NULL; cout << "虚函数表地址:" << (int*)(&b) << endl; cout << "虚函数表 — 第一个函数地址:" << (int*)*(int*)(&b) << endl; // Invoke the first virtual function pFun = (Fun)*((int*)*(int*)(&b)); pFun(); /* (Fun)*((int*)*(int*)(&b)+0); // Base::f() (Fun)*((int*)*(int*)(&b)+1); // Base::g() (Fun)*((int*)*(int*)(&b)+2); // Base::h() */ } /* 实际运行经果如下: 虚函数表地址:0012FED4 虚函数表 — 第一个函数地址:0044F148 Base::f */
-
函数表项中,
1)第一项指向的实际是,该类所关联的
type_info
对象。2)其余也是最主要的:本类的虚函数表指针、父类的虚函数表指针(指向的是这个类中各个虚函数的入口地址),这张表解决了继承、覆盖的问题。
-
当子类继承了父类时,会继承其虚函数表(即父类虚函数表的指针);当子类重写父类中虚函数时,会将其继承到的虚函数表中的地址替换为重写的函数地址。
一般继承:
- 无重载:虚函数按照声明的顺序存放在虚函数表中;父类的虚函数在子类的虚函数前面。
- 有重载:覆盖的f()函数被放到了虚表中原来父类虚函数的位置;没有覆盖的照旧。
多重继承:
- 无重载:每个父类都有自己的虚表;子类的虚函数在第一个父类的虚函数表中(所谓第一个父类是按照声明顺序来判断)。
- 有重载:三个父类虚函数表中的f()的位置被替换成了子类的函数指针(这样可用任一父类指针/引用指向子类,并调用子类的f());没有覆盖的照旧。
-
定义一个对象指针,就能够调用父类以及各个子类的同名同参函数,该对象指针必须是父类指针。
如果想通过父类指针调用子类中的同名同参函数,则父类中的函数声明前必须加virtual声明,即让该同名同参的成员函数为虚函数。
-
override
(虚函数专用):用于子类覆盖父类的同名同参成员函数。虚函数声明后加override,编译器就会在父类中找同名同参的虚函数,这样如果子类中的虚函数不小心写错则编译器能及时发现并报错。
-
final
(虚函数专用):当父类中某个成员函数声明后加final,那么任意尝试覆盖该函数的操作都会引发报错。
纯虚函数:
// 基类不能给虚函数有意义的实现,则将其声明为纯虚函数
virtual 函数返回类型 函数名(参数列表) = 0;
-
在基类中声明但未定义的纯虚函数,要求任何派生类都要重定义该虚函数。否则,派生类也会变成抽象基类,不能实例化,只能创建指针和引用。
-
含纯虚函数的抽象基类,不能实例化对象,但可以创建指针和引用(用来**“指向派生类对象”或“引用派生类对象”**)。
#include <iostream> using namespace std; // 抽象基类 class A { public: virtual void Display() = 0; }; class B : public A { public: virtual void Display() { cout << "B" << endl; } }; int main() { // A a; // 会报错,含有纯虚函数的抽象基类“不能实例化,只能创建指针和引用” B b; // 基类指针指向派生类对象,并调用派生类中重写了的基类成员函数 A* a1 = &b; a1->Display(); // 基类引用派生类对象,并调用派生类中重写了的基类成员函数 A& a2 = b; a2.Display(); return 0; }
-
纯虚函数所在的类,会变成抽象类,不能也没必要产生类对象。
- 在类的层次结构中,顶层或最上面几层可以是抽象基类,其体现了本类族中各类的共性。
- 把各类中共有的成员函数集中在抽象基类中声明。
- 抽象基类是本类族的公共接口,即是从同一基类中派生出的多个类有同一接口。
讨论使用虚函数的必要性:
- 如果要在派生类中重定义基类的函数即要考虑“多态”时,则将它设置为虚函数,否则不要设置;
- 两点好处:普通成员函数(地址是静态的,即编译时已确定)调用效率更高、可指明不要重定义该函数。
继承构造函数:
构造函数不能被继承,只能在创建函数对象时,通过派生类构造函数的初始化列表,显式的调用其直接基类的构造函数,否则会调用基类的默认构造函数。
析构函数也不能被继承,而销毁派生类对象时,会先执行派生类对象的析构函数,再自动执行基类的析构函数。手工调用派生类的析构函数,也会自动调用基类的析构函数。
c++11新增的继承构造函数的方式(不建议使用):
-
如果基类中含有多个构造函数,多数情况之下会继承所有的构造函数,如下例外:
- 如果在派生类中定义的构造函数与基类中的构造函数有相同的参数列表,那么基类继承来的构造函数会被派生类中的构造函数覆盖掉。
- 默认、拷贝、移动构造函数、赋值运算符,不能被继承,而是编译器按照合成规则自动合成。
-
using A::A
,其中using
的目的就是让某个名字在当前作用域内可见。继承的A的构造函数都会生成一个与之对应的派生类构造函数。
如果基类A的构造函数的参数列表中,含有默认参数的话,编译器再遇到
using A::A;
时,会在派生类中生成多个构造函数:1)带有所有参数的构造函数。
2)其余的构造函数,是由源参数列表随机删除默认参数,组合而成的多个构造函。
如果类B中,只含有
using A::A;
(从A中继承的构造函数),编译器会合成默认构造函数。
多重继承(从多个父类中产生子类):
多重继承:
-
派生类会包含每个基类的子对象;
-
如果在派生类中,重定义基类同名同参的函数,则会覆盖掉基类的同名同参函数;
-
通过增加作用域,可以明确的告诉系统调用的是基类c1、基类c2的成员函数;
C3 c3; c3.C1::myInfo(); c3.C2::myInfo();
派生类的构造函数和析构函数:
-
构造一个派生类对象,将同时构造并初始化所有的基类子对象;
-
每个派生类都只初始化它的直接基类,从而使所有类都得到初始化;
-
通过派生类构造函数的初始化列表,将实参传递给直接基类;基类构造顺序和派生类构造函数的参数列表中,基类出现顺序一致;
-
概念:显示/隐式类型转换
// 显示的使用C2的有参构造函数来初始化基类子对象 C3(int i, int j, int k) :C1(i), C2(j), m_valueC3(k) // 隐式的使用C2的默认构造函数(不带参数的构造函数)来初始化基类子对象 C3(int i, int j, int k) : C1(i), m_valueC3(k)
从多个父类继承构造函数:子类要定义同参构造函数的自己版本。
虚基类、虚继承:
#include <iostream>
using namespace std;
class A
{
public:
A(int i) :m_valueA(i)
{
cout << "调用类A的有参构造函数" << endl;
}
virtual ~A()
{
cout << "调用类A的虚析构函数" << endl;
}
void myInfo()
{
cout << m_valueA << endl;
}
public:
int m_valueA;
static int m_staticA; // 声明静态成员变量
};
int A::m_staticA = 40; // 定义静态成员变量
// virtual的作用:表示后续从类B、类C中派生的子类,共享一份虚基类A类
class B : virtual public A // 类B从A虚继承
{
public:
B(int i) : A(i), m_valueB(i)
{
cout << "调用类B的有参构造函数" << endl;
}
virtual ~B()
{
cout << "调用类B的虚析构函数" << endl;
}
void myInfo()
{
cout << m_valueB << endl;
}
public:
int m_valueB;
};
class C : virtual public A // 类C从A虚继承(每个A的子类都要虚继承A类)
{
public:
C(int i) : A(i), m_valueC(i)
{
cout << "调用类C的有参构造函数" << endl;
}
virtual ~C()
{
cout << "调用类C的虚析构函数" << endl;
}
void myInfo()
{
cout << m_valueC << endl;
}
public:
int m_valueC;
};
// 虚基类Grand是由最底层的派生类初始化的
class D : public B, public C
{
public:
D(int i, int j, int k, int h) : A(i), B(i), C(j), m_valueD(h)
{
cout << "调用类D的有参构造函数" << endl;
}
virtual ~D()
{
cout << "调用类D的虚析构函数" << endl;
}
void myInfo()
{
cout << m_valueD << endl;
}
public:
int m_valueD;
};
int main()
{
D d(1,2,3,4);
return 0;
}
菱形继承:派生类通过它的两个直接基类 分别继承 同一个间接基类。
产生的问题:会产生二义性/数据冗余的问题,即D类中能够直接当访问到两个m_a变量。
通过 “虚继承” 可以解决这个问题:
虚基类A:
- 无论这个类在继承中出现了多少次,派生类中都只会包含唯一的共享的一个虚基类A。
- 每个A的子类B、C都要虚继承它,这才能保证A的孙类能够虚继承A类。
- 注意事项:
- 虚基类A的初始化问题:虚基类A是由最底层的派生类初始化;
- 初始化的顺序问题:先初始化虚基类,再根据初始化列表的顺序进行初始化;
RTTI运行时类型识别(run time type identification):
为程序在运行阶段确定对象的类型,只适用于包含虚函数的类。
通过运行时类型识别检查,程序能够使用基类的指针或者引用来检查其所指向的对象的实际派生类型。
#include <iostream>
using namespace std;
class A
{
public:
virtual void func()
{
cout << "A::func()" << endl;
}
virtual void Display() = 0;
virtual ~A() { cout << "~A()" << endl; }
};
class B : public A
{
public:
virtual void Display()
{
cout << "B" << endl;
}
void func()
{
cout << "B::func()" << endl;
}
void func2()
{
cout << "B::func2()" << endl;
}
~B() { cout << "~B()" << endl; }
};
int main()
{
// A a; // 会报错,含有纯虚函数的抽象基类“不能实例化,只能创建指针和引用”
B b;
// 基类指针指向派生类对象,并调用派生类中重写了的基类成员函数
A* a1 = &b; a1->Display();
// 基类引用派生类对象,并调用派生类中重写了的基类成员函数
A& a2 = b; a2.Display();
a1->func();
// 通过dynamic_cast强制转换“基类指针/引用”为“派生类指针/引用”
B* b2 = dynamic_cast<B*>(a1);
b2->func2();
// 通过c语言风格的强制转换,将“基类指针/引用”转换为“派生类指针/引用”
B* b3 = (B*)a1;
b3->func2();
return 0;
}
-
dynamic_cast
:能够将基类指针/引用,安全的转换为派生类的指针/引用。// 强制转换成功,则返回对象的地址,失败则返回nullptr 派生类指针 = dynamic_cast<派生类类型*>(基类指针);
-
c语言风格的强制类型转换,将基类指针/引用,转换为派生类指针/引用,但必须保证目标类型正确。
RTTI
在工作时,只适用于包含虚函数的类,因为其要通过调用虚函数表完成操作,即只适用于多态类型。
typeid
运算符:返回指针/引用,所指对象的实际类型。
typeid(类型[指针/引用])
/typeid(表达式)
:会返回一个常量对象(是一个标准库类型type_info
(类/类类型))的引用。- 如果基类中不含虚函数,
typeid()
返回的是表达式的静态类型(定义时的类型)。 - 如果基类中含有虚函数,可通过
typeid(派生类类型)==typeid(基类指针指向的对象)
,判断基类指针指向的对象是否是派生类类型。
每一个虚函数表前,都有一个指针指向type_info
,负责对RTTI
的支持。
/*
type_info(类/类类型)
1. name()成员函数
2. 重载了==、!=运算符,用于对类型进行比较
*/
基类 *ptr = new 派生类;
const type_info &tp = typeid(*ptr);
cout << tp.name() << endl;
要想RTTI
的两个运算符正常工作,那么基类中至少有一个虚函数(只有虚函数的存在,这两个运算符typeid和type_info
才会使用指针/引用所绑定的对象的动态类型(new的类型))。
类与类之间的关系:
inheritance继承,表示is-a:
constructor and destructor
:
- Derived的构造函数,会首先自动调用Base的默认构造函数,然后执行Derived自己;
- 如果想指定调用Base的某个重载的构造函数版本,则需要自己指定调用哪个;
- Derived的析构函数首先会执行自己,然后编译器会自动调用Base的析构函数;
- base class的析构函数,必须是virtual虚函数,即基类的析构函数必须为虚析构函数;
inheritance with virtual functions
:
-
non_virtual非虚函数:不希望derived class重新定义(override重写)它;
-
virtual虚函数:含有默认定义,且derived class可以重新定义(override重写)它;
-
pure virtual纯虚函数:没有默认定义,故derived class必须重新定义(override重写)它;
注:含有纯虚函数的类为抽象类,不能产生类对象。
composition复合/组合,表示has-a:
template<class T, class Sequence = deque<T>>
class queue
{
...
protected:
// 采用组合的形式,关联deque类
Sequence c;
public:
bool empty() { return c.empty(); }
size_type size() const { return c.size(); }
reference front() { return c.front(); }
reference back() { return c.back(); }
// deque两端均可进和出,queue两端可分别进出
void push(const value_type& x) { c.push_back(x); }
void pop() { c.pop_front(); }
};
-
Container构造函数执行时,编译器会先自动调用Component的默认构造函数,然后执行Container自己。
如果想指定调用Component的某个重载的构造函数版本,则需要自己指定调用哪个。
-
Container的析构函数首先会执行自己,然后编译器会自动调用Component的析构函数。
delegation委托(又称Composition by reference):
委托的关系的由来:某个类要干的事,委托给另一个类。
#include <iostream>
using namespace std;
class A
{
public:
A()
{
cout << "A::A()" << endl;
}
void func()
{
cout << "A::func()" << endl;
}
};
class B
{
public:
B() : a(new A())
{
cout << "B::B()" << endl;
}
void func()
{
a->func(); // B类需要干的事,交给A类来干
}
private:
A* a;
};
int main()
{
B b;
b.func();
return 0;
}
典型应用:STL库中的string类模板的底层结构。
两个类互相依赖时:
类的前向说明:
class A2; // 类A2的前向说明,并不是类的完整定义
class A1
{
public:
A2* a2;
};
class A2
{
public:
A1* a1;
};
有些情况下,必须要类的完整定义而不是类的前向声明,
- 类A1的定义中,查看类A2的大小;
- 类A1的定义中,调用类A2的成员函数;
可调用对象、std::function类模板:
可调用对象(函数指针、函数对象、匿名函数lambda表达式):
c++倾向使用函数对象/lambda表达式,作为可调用对象。
int func(int val)
{
cout << "调用了func()函数" << endl;
return val;
}
class TC
{
public:
using tfpoint = void(*)(int); // tfpoint是函数指针类型
public:
TC()
{
cout << "调用了TC类的默认构造函数" << endl;
}
static void func(int val)
{
cout << "调用了TC类的静态成员函数func(),val = " << val << endl;
}
void operator()(int val)
{
func(val);
}
void operator()(tfpoint tmp_tfpoint, int val)
{
tmp_tfpoint(val);
}
void ptfunc(int val)
{
cout << "调用了TC类的普通成员函数ptfunc()" << endl;
}
};
void test1()
{
// 1)函数指针
int(*ptr)(int) = &func;
int result = (*ptr)(5);
cout << result << endl;
using tfpoint = int(*)(int); // tfpoint是函数指针类型
tfpoint fptr = func; // 函数指针类型
cout << "val = " << fptr(5) << endl;
cout << "................." << endl;
// 2)具有operator()成员函数的类对象(仿函数)
TC tc;
tc(5); // 等价于tc.operator()(5);
cout << "................." << endl;
// 3)可被转换为函数指针的类对象
tc(TC::func, 5);
// 4)类成员函数指针
void (TC::*myfuncPoint)(int) = &TC::ptfunc; // 类成员函数指针定义时,类似于普通的函数指针,只是增加了作用域
(tc.*myfuncPoint)(5); // 调用时,需要定义类对象,并通过类对象来实现类成员函数指针的调用
// 等价于tc.ptFunc(12);
}
-
函数指针作为其他函数的参数(将函数指针定义为函数指针类型);
// 函数指针做其他函数的参数: // 函数指针想要当作函数的参数,就要将函数指针定义为函数指针类型 int add(int i, int j) { return (i + j); } // 定义函数指针类型 typedef int(*FuncPtrType)(int, int); // 等价于using FuncPtrType = int(*)(int, int); // 写法一: void test(int i, int j, FuncPtrType funcPtr) // funcPtr就是函数指针 { int result = funcPtr(i, j); // 指针类型的变量,相当于调用函数 cout << "funcPtr(" << i << ", " << j << ") = " << result << endl; } // 写法二: void test(int i, int j, int(*funcPtr)(int a,int b)) // int(*funcPtr)(int a,int b)是回调函数 // funcPtr就是函数指针 { int result = funcPtr(i, j); // 指针类型的变量,相当于调用函数 cout << "funcPtr(" << i << ", " << j << ") = " << result << endl; } // 调用test函数 test(12, 13, add);
注:
- 函数指针
int(*p)(int)
:指向一个函数的入口地址; - 指针函数
int*p(int)
:返回值为一个指针;
定义函数指针的方式:
#include <iostream> using namespace std; int func(int val1, int val2) { return val1 + val2; } int main() { // typedef定义函数类型 typedef int(f)(int,int); f* fPtr1 = &func; // typedef定义函数指针类型 typedef int(*fPtrType)(int,int); fPtrType fPtr2 = func; // 声明函数指针: int(*fPtr3)(int,int); fPtr3 = func; // 定义函数指针 // 通过右值,auto能自动推导出函数指针类型 auto fPtr4 = func; cout << fPtr1(1,2) << endl; cout << fPtr2(1, 2) << endl; cout << fPtr3(1, 2) << endl; cout << fPtr4(1, 2) << endl; return 0; }
使用场景:
-
给排序函数定义一个比较函数的函数指针作为参数;
int array[5] = {1,2,3,5,4}; vector<int> vctor({1,2,3,5,4}); bool cmp_func(const int& a, const int& b) { return a < b; } // 1. 用“函数指针”作为可调用函数对象 sort(array, array+5, cmp_func); // sort(vctor.begin(), vctor.end(), cmp_func); // 2. 用“lambda表达式”作为可调用函数对象 sort(array, array+5, [](const int& a, const int& b) { return a < b; }); // sort(vctor.begin(), vctor.end(), [](const int& a, const int& b) { // return a < b; // }); struct cmp { bool operator()(const int& a, const int& b) const { return a < b; } }; // 3. 用“仿函数”作为可调用函数对象 sort(array, array+5, cmp); // sort(vctor.begin(), vctor.end(), cmp);
-
设置回调函数,即“发生某事件时调用该函数”;
int func(int val) { cout << "调用了func()函数" << endl; return val; } void test1() { int(*ptr)(int val); ptr = func; // 等价于int(*ptr)(int val) = &func; int result = (*ptr)(5); cout << result << endl; }
- 函数指针
-
仿函数:具有类内重载
operator()
成员函数的类对象(又称函数对象),能够行使函数的功能;class T { public: T(int val) { cout << "调用了类T的有参构造函数" << endl; } void operator()(int val) { cout << "调用了类T的重载()运算符void operator()(int val)" << endl; } void operator()(int val, int k) { cout << "调用了类T的重载()运算符void operator()(int val, int k)" << endl; } }; // 圆括号()就是函数调用最明显的标志,称为“函数调用运算符” void test() { T t(3); // 调用了类T的有参构造函数 // 如果类中重载了函数调用运算符(),那么我们就可以像使用函数一样使用该类的对象了 // 如果类中重载了圆括号(),该类就会变成可调用的,且允许有多个版本的重载()运算符的出现 t(3); // 调用了重载运算符,等价于t.operator()(3); t(3, 3); // 等价于t.operator()(3, 3); }
-
c++11引入的匿名函数,即闭包lambda表达式;
定义:一种可调用对象,lambda表达式定义了一个“匿名函数”,包含:“捕获当前作用域中的变量”、参数列表、返回值。
// 语法形式如下: auto funcObj = [capture](params) -> ret { body; }; // capture是捕获列表(控制lambda表达式,能够访问的外部变量,以及如何访问这些变量) // params是参数列表,ret是返回值类型,body是函数体 /* lambda表达式,对于 “能访问的外部变量的控制” 非常细致: 捕获列表(访问父作用域中的非静态局部变量,静态变量/全局变量可以直接访问): 1. [] 不捕获任何变量; 2. [&] 捕获外部作用域中所有变量,并作为引用在函数体中使用(按引用捕获); 3. [=] 捕获外部作用域中所有变量,并作为副本在函数体中使用(按值捕获); 4. [=, & foo] 按值捕获外部作用域中所有变量,并按引用捕获 foo 变量; 5. [bar] 按值捕获 bar 变量,同时不捕获其他变量; 6. [this] 捕获当前类中的 this指针,让lambda表达式拥有和当前"类成员函数同样的访问权限",即可在 lamda 中使用当前类的成员函数和成员变量; 7. [变量名]按值的方式捕获该变量; // *捕获列表的第一个参数是“默认捕获方式”,“其他的捕获方式要加变量”(变成显示捕获) 8. [&, 变量名]按引用捕获所有外部变量,但对变量名命名的变量以值捕获; 9. [=, &变量名]按值捕获所有外部变量,但对变量名命名的变量以引用捕获; 10. 静态变量不需要(也不能)捕获(因其生命周期在程序运行期间一直存在),可直接在lambda表达式中使用,使用则类似于“引用”捕获; #include <iostream> #include <vector> #include <functional> using namespace std; int main() { vector<function<bool(int)>> fvctor; { srand((unsigned)time(NULL)); static int randInt = rand() % 10; // 静态(局部)变量randInt ∈ [0,9] fvctor.push_back([](int val) { randInt++; // lambda表达式对静态(局部)变量的使用,类似于“引用”捕获 cout << randInt << endl; return (val > randInt); }); cout << fvctor[0](10) << endl; } // randInt为静态变量(生命周期一直到程序结束),即使超出作用域,引用捕获的randInt也不会失效 cout << fvctor[0](10); return 0; } */
闭包(函数中的函数):可以将它看作带有
operator()
的类类型对象,也就是仿函数(即函数对象)(故可以使用std::function
和std::bind()
来保存、调用lambda表达式)。本质:当编写一个lambda表达式后,编译器会将其翻译成一个类,且该类中有重载了
operator()
的成员函数。int x = 0, y = 1; auto lambda_ = [=x, &y](int z)->int { cout << x << ", " << y << ", " << z << endl; return 0; ;} lambda_(2); // 调用lambda_表达式函数 /* 编译器根据lambda表达式的定义,构建出来的匿名类(闭包类型) */ class Anonymous { public: Anonymous(int& x, int y) : x_(x), y_(y) { } int operator()(int z) { cout << x << ", " << y << ", " << z << endl; return 0; } private: int& x_; // 采用“引用捕获”时,lambda表达式直接引用即可,但要保证引用的对象有效; int y_; // 采用“值捕获”时,lambda函数生成的类中,用捕获变量的值初始化成员变量; }; Anonymous anonymous(x, y); anonymous(2);
1)采用“值捕获”时,lambda函数生成的类,用捕获变量的值初始化成员变量;
int a = 10; int b = 20; auto add = [=](const int c)->int { return a + b + c; }; cout << add(30) << endl; // 等价于:默认情况下,lambda表达式生成的类中是const成员函数,故不可改变变量的值。加上mutable就可以让以值捕获的变量能够在函数体中修改。 class Add { private: int m_a; int m_b; public: // 对应通过捕获列表捕获的变量值,按值捕获 Add(int a, int b) : m_a(a),m_b(b) { } int operator()(const int c) { return m_a + m_b + c; } }; Add add(a, b); cout << add(30) << endl;
2)采用“引用捕获”是,lambda表达式直接引用即可,但要保证引用的对象有效;
#include <iostream> #include <vector> #include <functional> using namespace std; int main() { vector<function<bool(int)>> fvctor; { srand((unsigned)time(NULL)); int randInt = rand() % 10; // randInt ∈ [0,9] fvctor.push_back([&randInt](int val) { return (val > randInt); }); cout << fvctor[0](10); } //cout << fvctor[0](10); // error:因超出作用域后,引用捕获的randInt失效,故无法正确调用!!! return 0; } // 修正: // 10. 静态变量不需要(也不能)捕获(因其生命周期在程序运行期间一直存在),可直接在lambda表达式中使用,使用则类似于“引用”捕获; int main() { vector<function<bool(int)>> fvctor; { srand((unsigned)time(NULL)); static int randInt = rand() % 10; // 静态(局部)变量randInt ∈ [0,9] fvctor.push_back([](int val) { randInt++; // lambda表达式对静态(局部)变量的使用,类似于“引用”捕获 cout << randInt << endl; return (val > randInt); }); cout << fvctor[0](10) << endl; } // randInt为静态变量(生命周期一直到程序结束),即使超出作用域,引用捕获的randInt也不会失效 cout << fvctor[0](10); return 0; }
总结:
-
凡是按值捕获的外部变量,在lambda表达式定义时,这些外部变量就被复制了一份存储在lambda表达式中。解决方法:
1)按引用捕获;
2)lambda表达式结合mutable使用,可让以值捕获的变量能在函数体中修改;(此外,除了引用传递,否则lambda表达式内部无法修改外部变量)
int x = 5; auto func = [=]() mutable { x = 6; return x; };
-
按引用& / 值=捕获,都可以访问类成员和作用域外的变量;但只有按引用捕获的变量才能在lambda表达式中修改;
-
不捕获任何变量的lambda表达式(即捕获列表为空)时,可转换成一个普通的函数指针;
using functype = int(*)(int); // 定义一个函数指针类型 functype ft = [](int val) { return val; }; cout << ft(15) << endl;
-
lambda表达式与std::function()与std::bind()函数结合使用;
std::function<int(int)> fc1 = [](int val) { return val; }; cout << fc1(0) << endl; // 0 std::function<int(int)> fc2 = std::bind([](int val) { return val; }, 16); cout << fc2(0) << endl; // 16 std::function<int(int)> fc3 = std::bind([](int val) { return val; }, std::placeholders::_1); cout << fc3(0) << endl; // 0
调用方式:
1)lambda表达式与普通函数的调用方法相同,都是使用()这种函数运算符;
2)直接将lambda表达式插入函数参数列表中,返回值作为函数实参;
-
-
可被转换为函数指针的类对象
#include <iostream> using namespace std; class TC { public: TC() { cout << "TC的默认构造函数执行了" << endl; } TC(const TC &tc) { cout << "TC的拷贝构造函数执行了" << endl; } void operator()(int val) { cout << "TC::operator()执行了,val = " << val << endl; } int operator()(int i, int j) { cout << "TC::operator()执行了" << endl; return i + j; } void ptFunc(int val) { cout << "TC::ptFunc()执行了, value = " << val << endl; } public: int m_a; }; class TC2 { public: // 定义一个函数指针类型: typedef void(*tfPoint)(int); // 等价于using tfPoint = void(*)(int); static void mySFunc(int val) // 静态成员函数 { cout << "TC2::mySFunc执行了,value = " << val << endl; } operator tfPoint() { return mySFunc; } }; int main() { TC2 tc2; tc2(12); // 先调用tfPoint,再调用mySFunc;也是可调用对象 // 等价于tc2.operator TC2::tfPoint()(12); return 0; }
-
类成员函数指针
#include <iostream> using namespace std; class TC { public: TC() { cout << "TC的默认构造函数执行了" << endl; } TC(const TC &tc) { cout << "TC的拷贝构造函数执行了" << endl; } void ptFunc(int val) { cout << "TC::ptFunc()执行了, value = " << val << endl; } public: int m_a; }; int main() { TC tc; // 类成员函数指针变量的定义和初始化 void(TC::*ptFuncPtr)(int) = &TC::ptFunc; (tc.*ptFuncPtr)(20); return 0; }
把这些可调用对象的指针保存起来,目的是方便随时调用“可调用对象”,像极了函数指针。
int func(int val)
{
cout << "调用了func()函数" << endl;
return val;
}
void test2()
{
map<string, int(*)(int)> map_operator;
map_operator.insert(make_pair("1", func));
for (auto iter = map_operator.begin(); iter != map_operator.end(); iter++)
{
iter->second(5);
}
}
std::bind()
绑定器:
定义:能够将对象以及相关的参数绑定在一起,绑定完成后可以直接调用,也可以用std::function<>
进行保存,待需要时使用。
格式:std::bind(待绑定的函数对象/函数指针/成员函数指针,参数绑定值1、参数绑定值2.....)
总结:
-
将可调用对象和参数绑定在一起,构成一个仿函数,需要时直接使用;
void myfunc(int i, int j) { cout << i << "\t" << j << endl; } auto func1 = std::bind(myfunc, 10, 20); func1();
-
如果函数有多个参数,可以绑定一部分参数,其他参数在调用的时候指定;
void myfunc(int i, int j) { cout << i << "\t" << j << endl; } auto func2 = std::bind(myfunc, placeholders::_1, 20); func2(11); auto func3 = std::bind(myfunc, placeholders::_1, placeholders::_1); func3(11); auto func4 = std::bind(myfunc, placeholders::_2, placeholders::_1); func4(11, 10);
-
绑定一个类成员函数时,需要传入类对象
class CT { public: CT() { cout << "调用了CT类的默认构造函数" << endl; } CT(const CT &tmp_ct) :m_val(tmp_ct.m_val) { cout << "调用了CT类的拷贝构造函数" << endl; } void func(int val) { m_val++; m_static_val++; cout << "调用了CT类的成员函数func(),val = " << val << endl; cout << "m_val = " << m_val << "\tm_static_val = " << m_static_val << endl; } public: int m_val = 0; static int m_static_val; }; int CT::m_static_val = 0; // std::bind()绑定一个类成员函数,需要传入类对象 CT ct; // 第二个参数ct类对象,会调用CT类中的拷贝构造函数,调用的是生成的“临时对象”中的func()函数 auto func6 = std::bind(&CT::func, ct, std::placeholders::_1); func6(10); // 第二个参数加入引用,则不会生成临时对象,故会func()中的操作会影响类成员变量 auto func7 = std::bind(&CT::func, &ct, std::placeholders::_1); func7(10); // 由于在调用类成员函数时,生成了临时对象,故func()函数中的任何操作均不会改变原类对象中的成员变量(静态成员变量除外(静态成员变量属于整个类)) cout << "m_val = " << ct.m_val << "\tm_static_val = " << ct.m_static_val << endl;
**注意:**std::bind()对预先绑定的参数是值传递的;依靠placeholders::_x
传递的参数是引用传递的。
void myfunc2(int &i, int &j)
{
cout << i++ << "\t" << j++ << endl;
}
/*
std::bind()传递参数的方式:
1)对预先绑定的参数,是值传递的;
2)依靠placeholders::_x传递的参数,是引用传递的
*/
int i = 10; int j = 11;
auto func4 = std::bind(myfunc2, placeholders::_1, placeholders::_2);
func4(i, j);
cout << i << "\t" << j << endl;
auto func5 = std::bind(myfunc2, i, placeholders::_1);
func5(j);
cout << i << "\t" << j << endl;
std::function类模板(“可调用对象包装器”):
std::function
(可调用对象包装器)是一个类模板:用来装各种可调用对象,例如:function<int(int)> f,f用来充当可调用对象。
int func(int val)
{
cout <<"调用了func()函数"<<endl;
return val;
}
class T
{
public:
T()
{
cout << "调用了类T的无参构造函数" << endl;
}
T(int val)
{
cout << "调用了类T的有参构造函数" << endl;
}
int operator()(int val)
{
cout << "调用了类T的重载()运算符int operator()(int val)" << endl;
return val;
}
void operator()(int val, int k)
{
cout << "调用了类T的重载()运算符void operator()(int val, int k)" << endl;
}
};
void test3()
{
// 函数作为可调用对象
function<int(int)> f1 = func;
cout << f1(5) << endl;
T t;
// 调用不同版本的重载()运算符,作为可调用对象
function<void(int, int)> f3 = t;
f3(5, 10);
function<int(int)> f2 = t;
cout << f2(5) << endl;
map<string, function<int(int)>> map_operator;
map_operator.insert(make_pair("1", func));
map_operator.insert(make_pair("2", t));
cout << map_operator["1"](12) << "\t" << map_operator["2"](12) << endl;
}
特点:通过指定模板参数,就能够实现用统一的方式来处理函数。
绑定不同的可调用对象:
-
绑定普通函数:
int func(int val) { cout << "调用了func()函数" << endl; return val; } // 绑定普通函数 function<int(int)> f1 = func; cout << f1(5) << endl;
*如果普通函数有重载,则无法放入function中,可通过函数指针解决。
#include <iostream> #include <functional> using namespace std; bool isZero(int val) { return val == 0; } bool isZero(double val) { return val == 0; } int main() { // 通过函数指针的过渡,避免函数重载带来的无法识别的问题 bool(*f_double)(double) = isZero; function<bool(double)> funcContainer1 = f_double; cout << funcContainer1(2) << endl; bool(*f_int)(int) = isZero; function<bool(int)> funcContainer2 = f_int; cout << funcContainer2(2) << endl; return 0; }
-
绑定类的静态成员函数(静态成员函数属于整个类)(静态成员函数指针,内部只能使用静态成员变量):
class T { public: T() { cout << "调用了类T的无参构造函数" << endl; } T(int val) { cout << "调用了类T的有参构造函数" << endl; } static void func(int val) { cout << "调用了T类的静态成员函数func(),val = " << val << endl; } int operator()(int val) { cout << "调用了类T的重载()运算符int operator()(int val)" << endl; return val; } void operator()(int val, int k) { cout << "调用了类T的重载()运算符void operator()(int val, int k)" << endl; } }; function<void(int)> f4 = T::func; f4(4);
-
*绑定普通成员函数、成员变量:
#include <functional> #include <iostream> using namespace std; class Foo { public: Foo(int num) : num_(num) {} Foo(const Foo& foo) { cout << "cctor:Foo(const Foo& foo)" << endl; num_ = foo.num_; } ~Foo() { cout << "dtor:~Foo()" << endl; } void print_add(int i) const { cout << num_ + i << '\n'; } void add(int i) { num_ += i; } int num_; }; // 1)常量对象,只能调用const修饰的成员函数; // 2)非常量类对象,const和非const修饰的成员函数均可调用; int main() { // 普通成员函数的调用 function<void(const Foo&, int)> f_add_display = &Foo::print_add; const Foo foo(314159); f_add_display(foo, 2); cout << ".................." << endl; // 数据成员访问器的调用: function<int(Foo const&)> f_num = &Foo::num_; cout << "num_: " << f_num(foo) << '\n'; cout << ".................." << endl; /* std::bind()对预先绑定的参数,是值传递的;依靠placeholders::_x传递的参数,是引用传递的 */ // 成员函数及对象的调用: 这里会调用两次拷贝构造函数, // 1)将foo2的临时对象副本传入Foo::add()中 // 2)第二次std::bind()本身会返回一个CT对象并拷贝给f_add_display2(此时临时对象被析构) Foo foo2(314159); function<void(int)> f_add_display2 = bind(&Foo::add, foo2, placeholders::_1); f_add_display2(2); cout << "num_: " << f_num(foo2) << '\n'; cout << ".................." << endl; // 成员函数和对象引用的调用: function<void(int)> f_add_display3 = bind(&Foo::add, &foo2, placeholders::_1); f_add_display3(2); cout << "num_: " << f_num(foo2) << '\n'; return 0; } /* // 结果为: 314161 .................. num_: 314159 .................. cctor:Foo(const Foo& foo) cctor:Foo(const Foo& foo) dtor:~Foo() num_: 314159 .................. num_: 314161 dtor:~Foo() dtor:~Foo() dtor:~Foo() */
-
绑定不同版本的重载
()
运算符(即仿函数):#include <iostream> #include <functional> using namespace std; class T { public: T() { cout << "调用了类T的无参构造函数" << endl; } T(int val) { cout << "调用了类T的有参构造函数" << endl; } static void func(int val) { cout << "调用了T类的静态成员函数func(),val = " << val << endl; } int operator()(int val) { cout << "调用了类T的重载()运算符int operator()(int val)" << endl; return val; } void operator()(int val, int k) { cout << "调用了类T的重载()运算符void operator()(int val, int k)" << endl; } }; int main() { T t; // 定义类对象 /* 绑定不同版本的重载()运算符 */ function<void(int, int)> f1 = t; f1(5, 10); function<int(int)> f2 = t; cout << f2(5) << endl; return 0; }
-
绑定
lambda
表达式:#include <functional> #include <iostream> using namespace std; int main() { std::function<void(int)> f_display = [](int val) { cout << val << endl; }; f_display(42); return 0; }
-
绑定
bind()
的调用结果:#include <functional> #include <iostream> using namespace std; void print_num(int i) { cout << i << '\n'; } int main() { function<void()> f_display_31337 = bind(print_num, 31337); f_display_31337(); return 0; }
std::bind()与std::function()结合使用:
-
将成员函数与参数的bind绑定结果,存放在std::function()中。
#include <iostream> #include <functional> using namespace std; class CT { public: CT() { cout << "dctor : CT()" << endl; } CT(const CT &tmp_ct) :m_val(tmp_ct.m_val) { cout << "cctor : CT(const CT &tmp_ct)" << endl; } void func(int val) { m_val++; m_static_val++; cout << "void CT::func(int val),val = " << val << endl; cout << "m_val = " << m_val << ",m_static_val = " << m_static_val << endl; } ~CT() { cout << "dtor : ~CT()" << endl; } void operator()() { cout << "void TC::operator()()" << endl; } public: int m_val = 0; static int m_static_val; }; int CT::m_static_val = 0; int main() { // 将成员函数与参数的绑定结果,存放在std::function()中 CT ct; cout << "......................." << endl; /* std::bind()对预先绑定的参数,是值传递的;依靠placeholders::_x传递的参数,是引用传递的 */ // 调用了两次拷贝构造函数: // 1)用来为ct生成临时对象; // 2)std::bind()本身会返回一个CT对象并拷贝给myfunction1(此时临时对象被析构) std::function<void(int)> myfunction1 = std::bind(&CT::func, ct, placeholders::_1); cout << "......................." << endl; // 并不会调用拷贝构造函数 std::function<void(int)> myfunction2 = std::bind(&CT::func, &ct, placeholders::_1); cout << "......................." << endl; // 调用了一次默认构造函数、拷贝构造函数: // 1)调用了默认构造函数用来构造临时对象, // 2)调用了拷贝构造函数生成了一个可调用对象作为std::bind()返回的仿函数类型对象,传给ct_1 auto ct_1 = std::bind(CT()); ct_1(); // 调用void TC::operator()() cout << "......................." << endl; return 0; } /* dctor : CT() ....................... cctor : CT(const CT &tmp_ct) cctor : CT(const CT &tmp_ct) dtor : ~CT() ....................... ....................... dctor : CT() cctor : CT(const CT &tmp_ct) dtor : ~CT() void TC::operator()() ....................... dtor : ~CT() dtor : ~CT() dtor : ~CT() */
-
将bind绑定结果作为实参,传入含有function包装形参。
#include <iostream> #include <functional> using namespace std; void func(int val) { cout << val << endl; } void myCallFunc(function<void(int)> funcObj, int val) { funcObj(val); } int main() { auto _func = std::bind(func, std::placeholders::_1); for (int i = 0; i < 10; ++i) { myCallFunc(_func, i); } return 0; }
区别:
- bind()主要是延迟调用,将可调用对象用统一格式保存起来,以备调用;
- function是可调用对象包装器;
c++11新标准:
新增long long/unsigned long long类型:
- VS中,int和long都是4bytes,long long是8bytes
- linux中,int是4bytes,long和long long是8bytes
原始字面量:
统一的初始化列表initializer_list:
#include<initializer_list>
// 拷贝、赋值:拷贝赋值一个initializer_list对象,并不会拷贝列表中的元素(即拷贝、赋值出的所有对象共享表中元素)
// 底层实现:底层使用array<T, size>来存储的
// **注:initializer_list中的元素永远是“常量值”,不能修改。**
使用范例:
-
c++11丰富了大括号的使用范围,用大括号括起来的列表(统一的初始化列表),可用于所有内置类型和用户自定义类型。
-
使用统一初始化列表时,也是用等号=,或者不使用。
int val = {10}; int val[5]{1,1,1,1,1}; double val{1.2;}
-
用于new表达式中。
int* arr = new int[4]{1,1,1,1};
-
创建类对象时。
Girl girl(12,"llli"); Girl girl{12,"yoyo"};
-
细节:
STL容器中,提供了initializer_list模板类作为参数的构造函数,使用时一般会进行隐式类型转换。
除了用于类构造函数外,还可将initializer_list用于常规函数的参数。
#include <iostream> #include <initializer_list> using namespace std; // 用于同类型参数的“可变参数函数” double sum(initializer_list<double> ils) { double total = 0.0; for (auto iter = ils.begin(); iter != ils.end(); ++iter) { //(*iter) += 1; // error:initializer_list中的元素永远是常量值,不能修改 total += (*iter); } return total; } int main() { double result = sum({1,2,3,4,5,6,7,8.1}); cout << result << endl; return 0; }
注:initializer_list中的元素永远是常量值,不能修改。
两种类型推导关键字auto、decltype:
自动推导类型auto(推导得到的类型会做任何变动):
编译器会确定auto和变量的类型,之后类型占位符auto会被推断出的auto的类型替换掉。
- 发生在编译时期,即并不会影响程序执行效率
- auto定义的变量必须立即初始化
auto非常灵活,可与指针、引用、const结合使用
// 传值方式(非指针、引用)
auto x = 10 --> auto : int、x : int
// 指针或引用(非万能引用)
const auto& x2 = x --> auto : int、x2 : const int&
auto x3 = x2 --> auto : int、x2 : int // 为传值方式
// 注:传值方式时,const和引用属性,均会被抛弃(传入的是副本)
// 万能引用:
auto&& x4 = x --> auto : int&、x : int&(x为左值,发生**“引用折叠”**(编译器自动处理))
auto&& x5 = 10 --> auto : int、x5 : int&&
auto使用常见的使用场景:
- 一般用于类型过长 或 表达式返回类型过于复杂(lambda表达式)的场景;
- 当类型不确定时,也可尝试使用auto,如:函数模板的返回值类型不确定时,可使用auto;
auto不适用的场景:
- 函数形参;
- 不能用来修饰普通成员变量,且使用auto推断的静态成员变量初始化必须在类内进行;
decltype关键字(推导得到的类型不会做任何变动):
c++11中,引入的decltype
操作符,用于查询表达式的数据类型。
// decltype通过分析表达式而得到它的类型,且“并不会执行表达式”
decltype(expression) var;
推导规则:
-
如果expression是一个没用括号括起来的标识符,则var的类型与该标识符的类型相同,包括const限定符;
-
如果表达式是一个左值(要排除第一种情况)、或者用括号括起来的标识符,那么var的类型时表达式的引用;
short a = 10; decltype(a) b; // type(a) == short // expression是括号括起来的标识符 decltype((a)) c = b; // type(c) == short& // expression是左值 decltype(++a) c = b; // type(c) == short&
-
如果表达式是一个函数调用,则var的类型与函数返回值的类型相同(函数返回值不能是void,但可以是void*);
-
上面的条件都不满足,则var的类型与expression的类型相同;
函数后置返回类型:
#include <iostream>
using namespace std;
template<typename T1, typename T2>
auto func(T1 x, T2 y) -> decltype(x+y)
// 其中decltype(x+y)只能是后置函数类型,这里的auto不具有类型推断的能力只是后置返回类型语法的一部分。
{
decltype(x+y) tmp = x + y;
return tmp;
}
int main()
{
cout << func<int, double>(1,1.2) << endl;
cout << func(1,1.2) << endl;
return 0;
}
元编程:
lambda表达式的类型推导:
auto cmp = [](const Person& person1, const Person& person2) -> bool {
return (person1.last_name < person2.last_name
|| (person1.last_name == person2.last_name
&& person1.first_name < person2.first_name);
};
set<Person, decltype(cmp)> table(cmp);
表达式类型推导,但并不会计算/执行表达式:
#include <iostream>
#include <vector>
using namespace std;
template<class Container>
class MyContainer
{
public:
decltype(Container().begin()) iter;
void GetBegin(Container& _container)
{
iter = _container.begin();
cout << *iter << endl;
}
};
int main()
{
vector<int> vctor = {1,2,3};
MyContainer<vector<int>> vctor_;
vctor_.GetBegin(vctor);
const vector<int> cvctor = {4,5,6};
MyContainer<const vector<int>> cvctor_;
cvctor_.GetBegin(cvctor);
return 0;
}
注:int i = 10; decltype((i)) j;
,则 j --> int&
。
auto中丢掉的东西,能通过decltype(auto)
捡回来,即decltype(auto)
。
#include <iostream>
using namespace std;
int func(int val1, int& val2)
{
++val2;
return val1 + val2;
}
template <typename F, typename... T>
//auto Func(F f, T&&... t) -> decltype(f(std::forward<T>(t)...)) // 存在丢失引用的可能
decltype(auto) Func(F f, T&&... t) // 解决上面提到的“引用丢失”的问题
{
return f(std::forward<T>(t)...);
}
int main()
{
int j = 10;
cout << Func(func, 20, j) << endl;
cout << j << endl;
return 0;
}
lambda表达式:
“lambda表达式”和“仿函数Functor”的实现对比:
explicit关键字:
用于关闭对象的自动类型转换,一般用于含有一个参数的构造函数。
noexcept:不抛出异常
growable containers
(会发生memory reallocation
),只有vector
和deque
。
final关键字(放在类名或虚函数名后):
用于限制某个类不能被继承,或者某个虚函数不能被重写。
override关键字:
在派生类中,把override写在成员函数的后面,表示重写了基类的虚函数,提高代码可读性。
基于范围for语句的循环:
for (auto x : vctor) {} // 序列vctor中,每个元素一次拷贝到x中
for (auto& x : vctor) {} // 相比之下,用引用省去了拷贝的动作,提高了系统的效率
for (const auto& x : vctor) {} // 如果不想循环体中,对x做任何修改,可加上const关键字修饰
默认函数控制=default与=delete:
在c++自定义的类中,编译器会默认生成一些成员函数:无参构造/拷贝构造/拷贝赋值/移动构造/移动赋值/析构函数。
class Girl
{
private:
int age;
string name;
public:
Girl() = default; // 使用编译器默认的构造函数
Girl(const Girl& girl) = delete; // 禁用该构造函数
~Girl() = default;
};
注意:如果类的成员变量中不含有指针成员变量,则使用默认的构造函数基本都可以。
别名alias:
类型的别名:typedef / using
typedef void(*funcPtr)(int);
using funPtr = void(*)(int);
using Vec= vector<int, allocator<int>>;
typedef vector<int, allocator<T>> Vec;
模板的别名:typedef / using
typedef std::vector<std::string>::iterator itType
using itType = std::vector<std::string>::iterator // c++11
区别:c++11中提出的using关键字,可用于模板部分偏特化,但typedef不能、define更不能(只是机械的替换)。
// 给偏特化版本的模板定义别名:
template<typename T>
using arr12 = std::array<T, 12>;
arr12<int> a1;
arr12<string> a2;
template<typename T>
using Vec = std::vector<T, MyAlloc<T>>;
Vec<int> vctor1;
Vec<string> vctor2;
模板模板参数:
#include <vector>
#include <iostream>
using namespace std;
// 含有“模板模板参数”的类模板
template <typename T, template <class> Container>
class AClass
{
public:
Container<T> container;
};
// 模板的别名
template <typename T>
using Vec = vector<T, allocator<T>>;
int main()
{
AClass<int, vec<int>> aClass;
return 0;
}
空指针nullptr(有更高的安全性):
- 空指针是不会指向有效数据的指针,之前c/c++中0既可以表示整型也可表示指针常量;
- c++11新增的
nullptr
关键字,表示空指针,其是指针类型不是整型类型; - c++11仍允许0表示空指针,因此
nullptr==0
该表达式是true;
智能指针:unique_ptr、shared_ptr、weak_ptr:
多用于多线程中。
强枚类型举(枚举类):
// 传统的枚举类
enum e1{red, green, blue}
// 传统的c++提供了一种创建常量的方式,但类型检查比较低级;且同一作用域内,两个枚举的成员不能相同。
// 针对枚举的缺陷,c++11引入了枚举类,又称强类型枚举
{
enum class e1{red,green,blue}
enum class e2{red,green, white, blue}
}
// 使用强枚举时,要在枚举成员名前面加枚举名和::,避免名称冲突
// 强枚举类型默认是int型,可以在声明时在枚举名后加:type(type不能是wchar_t类型)
数值类型和字符串类型之间的转换:
// 传统的方法:
sprintf() / snprintf() // 数值转char*字符串
atoi() / atol() / atof() // char*字符串转字符
// c++11提供的方法:
to_string() // 可将各种数据类型转换为string字符串类型:
stoi() / stol() / stoll() / stoul() / stoull() / stof() / stod() / stold() // string类型转为数值
常量表达式constexpr关键字:
- const关键字从功能上来说有双重语义:只读变量和修饰常量。
- 为了解决const的双重语义问题,c++11引入constexpr常量表达式,const表示“只读”、constexpr表示“常量”。
左值/右值,左值引用/右值引用:
左值:可出现在operator=的左/右侧。
右值:只能出现在operator=的右侧;可调用移动构造/移动赋值函数,可进行资源的转移。
左值引用和右值引用的区别:
-
c++中的垃圾回收GC机制
- RAII,通过“值语义”控制对象的生命周期,即通过局部对象来实现资源的交给系统自动回收。
- 右值引用可以丰富值语义,通过右值引用(移动语义)可将对象的生命周期延长到scope以外。
-
左值引用、右值引用的差异:
1、常量(左值)引用,可指向右值
2、
std::move()
,可将右值引用指向左值3、声明出来的左值、右值引用,都是左值
4、功能差异:
-
左值引用,可避免对象的拷贝(一般在传参时加常量左值引用,避免无用的对象拷贝)
-
右值引用:
1)移动语义(在移动构造函数和移动赋值函数中)
2)完美转发:函数模板可以将参数完美的转发给其内部调用的函数,即将值以及值的左右属性“透传”到内部调用的函数中。
实现机制:万能引用
&&
,利用了“引用折叠”规则;std::forward()
,还原值类型。#include <iostream> #include <cstring> using namespace std; void func(int&& val) { cout << "params are right value" << endl; } void func(int& val) { cout << "params are left value" << endl; } template<typename T> void funcLRVal(T&& val) // T&&作为参数类型,既可以接受左值,又可以接受右值 { func(val); } // 完美转发: template<typename T> void funcLVal(T& val) { func(val); } template<typename T> void funcRVal(T&& val) { func(std::move(val)); } template<typename T> void funcLRVal_(T&& val) // T&&作为参数类型,既可以接受左值,又可以接受右值 { func(std::forward<T>(val)); // 将左值转发后仍是左值引用,右值转发后仍是右值引用 } int main() { int a = 10; // 在模板函数”void funcLRVal(T&& val)“中,模板函数内部将参数转发给函数func()后,都变成了左值 //,故会丢失“左右值属性”导致”转发不完美“ funcLRVal(10); funcLRVal(a); cout << endl; /* 实现完美转发的两种方案: */ // 1、通过两个模板函数,分别实现右值和左值的转发 funcLVal(a); funcRVal(10); // 2、采用forward<T>转换 funcLRVal_(a); funcLRVal_(10); return 0; } /* params are left value params are left value params are left value params are right value params are left value params are right value */
-
委托构造和继承构造:
委托构造:
在一个构造函数的初始化列表中调用另一个构造函数。
#include <iostream>
#include <string>
using namespace std;
class A
{
private:
int a; int b; double c; string str;
public:
A(int a_, int b_) : a(a_),b(b_) { cout << "A(int a_, int b_)" << endl; }
A(int a_, int b_, string str_) : A(a_, b_)
// 使用委托构造,即委托A(int a_, intb_)构造函数来初始化a和b
{
str = str_;
cout << "A(int a_, int b_, string str_)" << endl;
}
A(double c_) : c(c_) { cout << "A(double c_)" << endl; }
A(double c_, string str_) : A(c_) // 使用委托构造,即委托A(double c_)构造函数来初始化c
{
str = str_;
cout << "A(double c_, string str_)" << endl;
}
};
int main()
{
A a1(1,2); A a2(1,2,"lili");
A a4(1.2); A a3(1.2, "yoyo");
return 0;
}
注意:
- 一旦使用了委托构造,就不能再在初始化列表中初始化其他的成员变量;
- 不要生成环状的构造过程;
继承构造:
#include <iostream>
#include <string>
using namespace std;
class A
{
private:
int a; int b; string str;
public:
A(string str_) : str(str_) { cout << "A(string str_)" << endl; }
A(int a_, int b_) : a(a_), b(b_) { cout << "A(int a_, int b_)" << endl; }
};
class B : public A
{
private:
int c;
//A a = {10,10}; // 类内初始化,每构造一个类对象都会初始化一次
public:
using A::A; // 派生类B中继承基类A的构造函数
B(int a_, int b_, int c_) : A(a_, b_), c(c_) { cout << "B(int a_, int b_, int c_)" << endl; }
B(string str_, int c_) : A(str_), c(c_) { cout << "B(string str_, int c_)" << endl; }
};
int main()
{
// 调用继承的类A的构造函数
B b1(10, 20);
B b2("yoyo");
cout << endl;
// 调用类B的构造函数,并在初始化列表中初始化类A
B b3(10, 20, 12);
B b4("yoyo", 20);
return 0;
}
/*
A(int a_, int b_)
A(string str_)
A(int a_, int b_)
B(int a_, int b_, int c_)
A(string str_)
B(string str_, int c_)
*/
- c++11之前,派生类如果要使用基类的构造函数,可以在派生类构造函数的初始化列表中指定;
- c++11之后,推出了新的继承构造函数
inherting constructor
,即在派生类中使用using来声明继承基类的构造函数;
可变参数模板Variadic template:
-
对参数进行了泛化,能支持任意个数、任意数据类型的参数。
-
如果传入的可变参数类型不同,则详见“泛型编程”。
-
如果传入的可变参数类型相同,则使用
initializer_list<T>
足以。#include <iostream> using namespace std; namespace _nmsp { template <typename _ForwardIterator, typename _Cmp> _ForwardIterator max_element(_ForwardIterator first, _ForwardIterator last, _Cmp cmp) { if (first == last) { return first; } _ForwardIterator result = first; while (++first != last) { if (cmp(*result, *first)) { result = first; } } return result; } template <typename T> T max(initializer_list<T> _I) { return *max_element(_I.begin(), _I.end(), std::less<T>()); } } int main() { cout << _nmsp::max({2,4,5,6,7,8}) << endl; return 0; }
资源管理方案RAII:
主流编程语言中,c++是唯一依赖RAII(resource acquisition is initialization)来做资源管理的。
RAII依托栈 和 构造/析构函数,来对所有资源(包括堆内存在内的内存进行管理),
- 将动态分配的资源的生命周期绑定到某个局部变量上,并随着作用域的创建和消失,完成分配和释放;
- 使用一个“包含动态数组的类”,作为局部对象变量,并在类的构造函数中new/析构函数中delete。
RAII有一些比较成熟的智能指针,借助“引用计数”来完成垃圾回收,比如std::auto_ptr(c++11已经废弃)、std::shared_ptr、std::weak_ptr、std::unique_ptr。
- 详细参考本博客:c++11-14-17_内存管理(RAII)。
指针:
指针的概念:
- 指针本身就是一个变量,其符合变量定义的基本形式,指针变量所在的地址中存储的是其指向的变量的地址。
- 一个类型
T*
的变量,能保存一个类型T
的对象的地址。 - 通过单目操作符
*
,可以间接的访问指针变量所指向的内存地址中的值,该过程称为间接访问/引用指针。
指针的运算:
指针的算数运算:p + i == p + i*d
(i为整数,d为p指向的变量占字节数),p指向的内存区域,存储相同的数据类型。
- 若p1与p2指向同一数组,
p1 - p2
= 两指针间元素个数 =(p1 - p2) / d
。 p1 + p2
无意义。
指针变量的赋值运算:
一维数组:
p = array
/p = &array[i]
/p=array,*(p++)
:将数组元素地址存放在地址变量p所在的地址。- 数组名array,表示数组首地址的地址常量。
- 变址运算符:
array[i] == *(array+i)
。
二维数组:
*(a[i]+j) == *(*(a+i)+j) == a[i][j]
、a+i == &a[i] <==> a[i] == *(a+i) == &a[i][0]
- 指针变量与其指向的变量具有相同数据类型。
指针的关系运算:若p1和p2指向同一数组,
p1<p2
,表示p1
指的元素在前;p1>p2
,表示p1
指的元素在后;p1==p2
,表示p1
与p2
指向同一元素;
变量、地址和指针变量:
- 一个变量有三个重要的信息:变量的地址信息、变量所存的信息、变量的类型。
- 指针变量,是专门用来记录变量地址的变量,通过指针变量可以间接得到另一个变量的值。
- 指针变量,必须先赋值再使用。
c++中的几种原始指针:
一般类型指针T*
:
int* t = &i;
cout << (*t) << endl;
array of pointers指针的数组和a pointer to an array数组的指针:
// 指针的数组T* t[]
int* t[];
cout << *(t[0]) << endl;
// 数组的指针T(*t)[]
int(*t)[];
cout << (*t)[0] << endl;
函数指针:
-
一个函数实际上为存放在内存中的一段程序,它有一个入口地址 –––– 函数的指针。
-
存放函数指针的变量 ––– 指向函数的指针变量。
-
使用方法:
1、函数指针的定义:
返回值类型(*函数指针变量名)(形参类型)
;2、函数指针的赋值:
函数指针变量名 = 函数名
;3、用函数指针变量调用函数:
(*函数指针变量名)(实参)
; -
函数指针变量作为函数参数:
当一个函数被调用后,执行过程中可以根据实参的函数名来调用不同的函数。
二级指针:**T=*(*T)
- 二级指针使用最多的场景是:
void GetMemory(char** ptr, int num)
{
*ptr = new char[num];
}
int main()
{
char* ptr = NULL;
GetMemory(&ptr, 10);
// 此时,ptr指向的是GetMemory中new出来的动态内存
}
- 函数的形参(二级指针)指向函数内部new的动态内存,函数外部的一级指针可以传入其地址来指向该动态内存。
- 这样即使函数调用返回后,会删除栈区该形参的地址内容,但二级指针任然可以正常指向new的动态内存。
三级指针:***T = *(*(*T))
nullptr
指针:一个特殊的指针变量,表示不指向任何东西。int* a = NULL
:用来表示a未指向任何东西。
原始指针的基本运算:
-
&
与*
操作符:注意:
*(p + 1)
与*p + 1
的区别。 -
++
与--
操作符:注意:
char* ch1 = ++ ch
、char* ch2 = ch++
的区别。 -
++++ 与 ---- 等运算符:
- 编译器程序分解成符号的方法是:一个字符一个字符的读入,如果该字符可能组成一个符号,那么读入下一个字符,一直到读入的字符不再组成一个有意义的符号。
- 编译器,采用的是“贪心法”。
野指针:
-
“野指针”不是NULL指针,是指向“垃圾”内存的指针
-
产生的原因:
1)指针变量没有被初始化:用指针进行间接访问之前,必须要确保它已经被初始化,并被恰当赋值,否则需要将指针指向NULL。
// 任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。 // 指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。例如 char *p = NULL; if (p != NULL) { cout << *p << endl; } char *str = (char *) malloc(100);
2)指针ptr被free或者delete之后,没有置为nullptr,让人误以为ptr是个合法的指针。
3)指针操作超越了变量的作用域范围。
总结:没有初始化的、不用的或者超出范围的指针,需要将指针的值置为nullptr。
解决出现空指针、野指针及内存泄露的问题:
-
使用更安全的指针 – 智能指针。
独占式指针unique_ptr(用来替代auto_ptr)
共享式指针shared_ptr,弱指针weak_ptr来配合shared_ptr使用(解决循环引用的问题)
-
不使用指针,使用引用的方式。
-
指针和引用在C++中都是用于间接访问其他对象或值的工具。然而,它们有一些关键的区别,这些区别使得在某些情况下使用引用比使用指针更好,反之亦然。
- 初始化:引用必须在声明时初始化,之后不能改变引用的对象。这使得引用更加安全,避免了悬挂引用(dangling reference)的问题。另一方面,指针可以在任何时候指向任何对象,这虽然提供了更大的灵活性,但也可能导致程序错误(例如空指针解引用或悬挂指针)。
- 间接访问:引用在语法上比指针更易于使用。使用引用时,你无需显式地解引用,就像使用原始对象一样。这可以使代码更简洁,减少出错的可能性。而指针则需要显式地解引用,这可能会使代码更复杂,并可能导致错误。
- 可空性:指针可以为空,引用不能。这使得指针可以用于表示可能不存在的对象,而引用则不能。在某些情况下,这是非常重要的。
- 指针可以指向多级间接性(例如,指向指针的指针),而引用只能是一级的。
因此,虽然指针和引用在功能上有一些重叠,但它们各自有独特的优点和用途,这就是为什么我们既需要指针又需要引用。
智能指针:
new/delete:
直接内存管理(new/delete成对存在的,作用是回收new分配的内存(释放内存)):
-
new、delete分配方式,称为动态分配(分配在堆上)
-
初始化:
// 初始化 int* iPtr1 = new int; // 初值未定义 int* iPtr2 = new int(); // “值初始化”(发现被初始化成 0) int* iPtr3 = new int(100); // 用圆括号给一个int的动态对象初值 string* myStrPtr1 = new string; // 空字符串,说明调用了string的默认构造函数 string* myStrPtr2 = new string(); // “值初始化”,也是空字符串 string* myStrPtr3 = new string("test"); string* myStrPtr4 = new string("test", 5); vector<int>* myVctorPtr = new vector<int>{ 1,2,3,4 }; // 值初始化与否,效果相同;都调用了类A的默认构造函数 A *a1 = new A; A *a2 = new A(); /* 总结:new对象的时候,最好进行值初始化(即在类名后加圆括号),防止它的值没被初始化 */
-
c++11中,auto可以和new配合使用:
// c++11中,auto可以和new搭配使用 string *myStrPtr5 = new string("test", 5); string **myStrPtr5_1 = new string *(myStrPtr5); // auto == string **; myStrPtr5 == string * auto myStrPtr5_2 = new auto (myStrPtr5); cout << myStrPtr5->c_str() << "\t" << (*myStrPtr5).c_str() << endl; cout << (*myStrPtr5_1)->c_str() << "\t" << (**myStrPtr5_1).c_str() << endl; cout << (*myStrPtr5_2)->c_str() << "\t" << (**myStrPtr5_2).c_str() << endl; // 指针myStrPtr5、*myStrPtr5_1、*myStrPtr5_2代表同一段内存 delete myStrPtr5;
-
const对象也可以动态分配内存:
// const对象也可以动态分配内存 const int* ciPtr = new const int(200); cout << "address = " << ciPtr << "\t" << "value = " << *ciPtr << endl;
-
一块内存只能delete一次(指向同一块内存的指针只能删除一次),且delete后这块内存就不能使用了:
int *ptr1 = new int(5); int *ptr2 = ptr1; delete ptr1; // delete ptr2; // 报错,因为ptr1、ptr2直接指向的是同一块内存,删除一次后就不能再删除了
注意:空指针可以多次删除。
-
不是new出来的内存,不能用delete;否则执行会报异常
int i2 = 10; int *Ptr = &i2; // 不是new出来的内存,不能用delete Ptr = nullptr; //delete Ptr; // 直接用delete会报错
-
释放申请的内存的过程:
1)释放了内存空间后,原来指向这块空间的指针还是存在!只不过现在指针指向的内容是垃圾(垃圾:是未定义的);
2)释放内存后把指针指向nullptr,防止指针在后面不小心又被解引用了;
delete ptr; ptr = nullptr; // 好习惯!! // 表明该指针不指向任何对象(提倡在delete后,给该指针一个nullptr)
-
总结:
-
1)new出来必须用delete释放且需要将指针变量指向nullptr,否则会发生内存泄露;
-
2)delete后的内存不能再使用;
-
3)同一块内存释放两次的问题;
new/delete具备在堆上,对所分配的内存空间进行初始化/释放的能力(而这种能力malloc/free并不具备):
// malloc、free(主要用于c语言中);new、delete(主要用于c++语言中);
// new/delete 比 malloc/free干的事更多
class A
{
public:
A()
{
cout << "调用了类A的默认构造函数A()" << endl;
}
virtual ~A()
{
cout << "调用了类A的虚析构函数~A()" << endl;
}
};
// new/delete具备 在堆上,对所分配的内存空间 进行初始化/释放的能力(而这种能力malloc/free并不具备)
void func()
{
A *Aptr = new A(); // new调用类A的默认构造函数
delete Aptr; // delete调用类A的析构函数
A *Aptr2 = (A*) malloc(sizeof(A));
free(Aptr2); // malloc/free:并未调用默认构造函数和析构函数
}
new/delete使用时,都干了两件事:
/* new内部的机制,会记录分配的内存大小,以供delete使用 */
// new干了两件事:a)分配内存;b)调用默认构造函数初始化内存;
// 分配 100bytes 内存
void *myPtr = operator new(100);
// delete也干了两件事:a)调用析构函数;b)释放内存(调用operator delete()来释放内存)
// 释放 100bytes 内存
operator delete(myPtr, sizeof(myPtr));
myPtr = nullptr;
- new干了两件事:a)分配内存;b)调用默认构造函数初始化内存;
- delete也干了两件事:a)调用析构函数;b)释放内存
new[]/delete[]数组:
// new[] 应该用 delete[]释放
// 内置类型比如int,不需要调用析构函数,所以new[]的时候系统并没有多分配 4bytes
int *ptr = new int[2]; // 堆区 申请 8bytes内存空间
delete[] ptr;
ptr = nullptr;
A a;
int alen = sizeof(a); // 类对象至少要占用 1bytes(成员函数不会占用分配内存(如果有虚函数,则会分配4/8字节的固定内存,用来存储虚函数表指针))
A *aPtr = new A[2](); // 占用 20bytes(两个类A占用 16bytes,但最终多占用了 4bytes ???)
// 系统用这 4bytes 来记录将要构造或析构的次数(即申请的类数组的大小:2)
delete[] aPtr;
aPtr = nullptr;
// 结论:如果一个对象(内置类型对象,比如int、float;类对象A,但没有自定义析构函数)(即没有多占用 4bytes 用来记录构造和析构的次数),则可以使用new[]来分配内存,delete释放内存(而非delete[])
//A *aPtr2 = new A[2]();
//delete aPtr2; // 不用delete[]删除new[]出来的内存,会报错(发生内存泄漏);
// 因为只能调用一次析构函数和调用一次operator delete(aptr2)(释放内存)
总结:new[] 应该用 delete[]释放(内部逻辑较为复杂,不考虑)
- c++11出现的智能指针,如果new后没有delete,智能指针能够帮你delete。
- 通过MFC应用程序,能够检查程序是否存在内存泄漏。
智能指针综述:
裸指针(直接用new返回的指针):这种指针比较灵活,但容易出错。
int *ptr = new int(); // ptr称为裸指针(直接用new返回的指针)
int *ptr2 = ptr;
int *ptr3 = ptr;
delete ptr; // 释放该段内存,会导致ptr2、ptr3都无法使用
智能指针:可以理解为给裸指针包装一层,用来解决裸指针可能出的各种代码的问题。
-
智能指针就是一个类,故对指针进行初始化时不能将一个普通指针直接赋值给智能指针。
- 可以通过
make_shared / make_unique
函数; - 通过构造函数传入
(new T())
; - 裸指针可以初始化
shared_ptr / unique_ptr
智能指针,但不推荐使用!!!
- 可以通过
-
智能指针的设计思路:
1)智能指针是类模板,在栈上创建智能指针对象;
2)把普通指针交给指针对象;
3)智能指针对象过期时,调用析构函数释放普通指针的内存; -
智能指针能够“自动释放所指向的对象内存,不再需要我们手动释放”,即能够帮助我们进行“动态的分配对象(new出来的对象)的生命周期管理,防止内存泄漏”。
-
智能指针的类型:
- c++11的标准:独占式指针unique_ptr(用来替代auto_ptr)、共享式指针shared_ptr、弱指针weak_ptr(配合shared_ptr使用,解决shared_ptr造成的循环引用的问题)。
- auto_ptr是c++98的标准,c++17已弃用。
c++标准库std::中,四种智能指针(都是类模板)
auto_ptr(c++98):
- c++11弃用,已被unique_ptr代替。
- 拷贝/赋值过程中,直接剥夺原对象对内存的控制权,转交给新的内存对象,同时将原对象指针置为nullptr,即发生了所有权转移。
- 所有权的转移,就会导致当该智能指针再次访问原对象时,程序会报错。
unique_ptr:独占式指针
独占式(专属所有权):同一时刻,只能有一个unique_ptr指针指向这个内存对象。
- 当unique_ptr被销毁时,所指向的内存对象也会被销毁。
- unique_ptr管理的内存,只能被一个对象所持有(不支持复制和赋值),但可以通过移动语义完成所有权的转移。
#include<memory>
中unique_ptr类模板:(禁用拷贝构造函数、赋值函数)
template<typename T, typename D=default_delete<T>>
class unique_ptr
{
public:
explicit unique_ptr(pointer p) noexcept; // 不可用于转换函数
~unique_ptr() noexcept;
T& operator*() const; // 重载*运算符
unique_ptr(unique_ptr&& up) noexcept; // 移动构造函数
void operator=(unique_ptr&& up) noexcept; // 移动赋值运算符函数
private:
pointer ptr; // 内置的指针
};
初始化:
格式:unique_ptr<指向的内存对象的类型> 智能指针变量名
-
常规初始化:unique_ptr和new配合使用
unique_ptr<int> iuptr1; // 指向int对象的一个空unique_ptr指针 if (iuptr1 == nullptr) { cout << "iuptr目前是空指针" << endl; } unique_ptr<int> iuptr2(new int(10));
-
make_unique()
函数(c++14中):不支持指定删除器。如果不需自定义删除器,建议优先使用make_unique()函数。unique_ptr<int> iuptr3 = make_unique<int>(10); auto iuptr4 = make_unique<int>(20); cout << typeid(iuptr4).name() << endl;
-
用普通指针来构造unique_ptr(不安全),可能会出现多次析构同一块内存,造成内存泄漏。
#include <iostream> #include <memory> using namespace std; int main() { int* ptr = new int(10); { unique_ptr<int> up(ptr); cout << *ptr << endl; } cout << *ptr << endl; delete ptr; return 0; }
unique_ptr常用操作:
#include <iostream>
#include <memory>
using namespace std;
// 需要一个指针,只是使用并不负责释放其指向的内存资源
template <typename T>
void func1(const T* ptr) { cout << *ptr << endl; }
// 需要一个指针,且会负责释放其指向的内存资源
template <typename T>
void func2(T* ptr) { delete ptr; }
// 需要一个unique_ptr指针,只是使用并不负责释放其指向的内存资源
template <typename T>
void func3(const unique_ptr<T>& uptr) { cout << *uptr << endl; }
// 需要一个unique_ptr指针,且会对该指针负责
template <typename T>
void func4(unique_ptr<T> uptr) { uptr=nullptr; }
int main()
{
unique_ptr<int> up(new int(10));
func1(up.get());
func2(up.release());
up = unique_ptr<int>(new int(10));
func3(up);
func4(std::move(up));
return 0;
}
-
unique_ptr不支持的操作(独占式):拷贝、赋值、+、-、++、–等;
unique_ptr<int> iuptr5 = make_unique<int>(10); // unique_ptr指针不支持拷贝动作: //unique_ptr<int> iuptr6(iuptr5); unique_ptr<int> iuptr7; // 指向int对象的一个空unique_ptr指针 // unique_ptr指针不支持赋值动作: //iuptr7 = iuptr5;
-
移动语义
std::move()
:unique_ptr<string> suptr1(new string("I love China!")); unique_ptr<string> suptr2(std::move(suptr1)); // 1)suptr1被置空 // 2)suptr2调用移动构造函数,使suptr1所指向的内存对象转交给了suptr2(没有发生任何拷贝和内存释放的操作)
-
release()
:返回裸指针并放弃对指针的控制权(切断智能指针与其所指向的内存对象的联系,此时智能指针被置空)。unique_ptr<string> suptr3(new string("I love China!")); // 返回的裸指针,用来初始化另一个智能指针 unique_ptr<string> suptr4(suptr3.release()); // 返回的裸指针,能够手动释放、也可以用来初始化另一个智能指针(或给另一个智能指针赋值) // 需要手动释放 string* sptr1 = suptr3.release(); delete sptr1; sptr1 = nullptr;
用途:可用于将unique_ptr传入子函数中,并由子函数负责释放对象。
-
get()
:返回智能指针中的保存的裸指针,但智能指针并不会被释放(裸指针和智能指针,指向同一块内存对象)。unique_ptr<string> suptr8(new string("test")); string* sptr2 = suptr8.get(); *sptr2 = "I love China!"; // 裸指针的智能指针,不会被释放;裸指针和智能指针,指向同一块内存对象 suptr8.reset(); //delete sptr2; // 不能既删除智能指针,又删除裸指针,不然同一块内存会释放两次,引发报错 sptr2 = nullptr;
注意:使用智能指针时,尽量不要再使用裸指针。
-
reset()
:// 1)reset()不带参数的情况:用来释放智能指针所指向的内存对象,并将智能指针置空 unique_ptr<string> suptr5(new string("I love China!")); suptr5.reset(); // 2)reset带参数的情况:释放智能指针所指向的内存对象,并指向新的内存对象 unique_ptr<string> suptr6(new string("I love China!")); suptr6.reset(new string("test")); unique_ptr<string> suptr7(new string("I love China!")); // 返回裸指针,用来初始化suptr7且suptr6被置空 suptr7.reset(suptr6.release());
-
=nullptr
:释放智能指针所指向的内存对象,并将智能指针置空。 -
指向一个数组:unique_ptr提供了支持数组的特化版本(即数组版本的unique_ptr),其重载了[]运算符(返回的是引用,可作为左值使用)。
// 数组类型后,要加[] unique_ptr<int[]> iarruptr(new int[10]); // 未指定初始值 //unique_ptr<int[]> iarruptr(new int[3]{0,1,2}); // 指定初始值 for (int i = 0; i < 10; i++) { cout << i << endl; iarruptr[i] = i + 1; // 通过观察内存发现,数组中的10个元素是紧密排列的(各占4bytes) }
-
*
解引用和->
:unique_ptr类模板重载了
*
和->
运算符,可以像使用普通指针一样使用unique_ptr。注意:定义的数组没有解引用,直接用suptr[i]就可以访问内存对象;
unique_ptr<string> suptr9(new string("I love China!")); *suptr9 = "test";
-
swap()
:交换两个智能指针所指向的内存对象。unique_ptr<string> suptr10(new string("I love China!10")); unique_ptr<string> suptr11(new string("I love China!11")); // 调用标准库函数std::swap() std::swap(suptr10, suptr11); // 调用swap():成员函数 suptr10.swap(suptr11);
-
智能指针名,作为判断条件;
-
临时unique_ptr的右值,赋值给unique_ptr(一般用于函数返回值):
// 将一个unique_ptr赋给另一个时,如果源unique_ptr是一个临时右值,编译器允许这样做,一般用于函数返回值 unique_ptr<T> func() { // 返回临时右值 return unique_ptr<T>(new T(...)); } int main() { unique_ptr<T> up; // 用临时右值,初始化unique_ptr指针对象 up = unique_ptr<T>(new T(...)); }
-
临时unique_ptr的右值,赋值给shared_ptr,进行类型转化:
auto myfunc() { return unique_ptr<string>(new string("I love China!")); // 临时对象都是右值 } // 因为shared_ptr中包含了显示构造函数,可用于将右值转换为shared_ptr shared_ptr<string> suptr13 = myfunc(); // shared_ptr将接管unique_ptr所指向的内存对象
删除器、尺寸:
指定删除器:
格式:unique_ptr<指向的对象类型,删除器> 智能指针变量名
。
void myDeleter(string *tempStr)
{
delete tempStr;
tempStr = nullptr;
// 打印日志
cout << "unique_ptr指针,调用了自定义的删除器myDeleter" << endl;
}
auto mylambdaDeleter = [](string *tempStr)
{
delete tempStr;
tempStr = nullptr;
// 打印日志
cout << "unique_ptr指针,调用了自定义的删除器mylambdaDeleter" << endl;
};
// 1)在类型模板参数中,写入类型名,然后再初始化的参数中给出具体的删除器
typedef void(*funcPtr)(string *); // 等价于using funcPtr = void(*)(string *),其中funcPtr是类型名typename
unique_ptr<string, funcPtr> suptr2(new string("I love China!"), myDeleter);
// 2)用lambda表达式实现删除器(lambda表达式可以理解为带有operator()的匿名类的对象)
unique_ptr<string, decltype(mylambdaDeleter)> suptr3(new string("I love China!"), mylambdaDeleter);
删除器额外说明:
- shared_ptr:就算两个shared_ptr指定的删除器不同,只要他们所指向的内存对象相同,那么这两个shared_ptr也属于同一个类型(即可以装在同一个容器中)。
- unique_ptr:指定unique_ptr中的删除器不同,会导致unique_ptr的类型不同(因为删除器类型名,直接被定义在类模板参数中了)(故不能放在同一个容器中)。
总结:shared_ptr指定的删除器,更加灵活,且不作为类型判断标准。
尺寸问题:
-
通常情况下,unique_ptr和裸指针一样,占用
8bytes
。 -
如果增加了自定义的删除器,则unique_ptr的尺寸可能会增加(lambda表达式作为删除器不会增加对象的内存占用字节数,而普通的函数指针会增加
8bytes
)。void myDeleter(string *tempStr) { delete tempStr; tempStr = nullptr; // 打印日志 cout << "unique_ptr指针,调用了自定义的删除器myDeleter" << endl; } auto mylambdaDeleter = [](string *tempStr) { delete tempStr; tempStr = nullptr; // 打印日志 cout << "unique_ptr指针,调用了自定义的删除器mylambdaDeleter" << endl; }; // 1)在类型模板参数中,写入类型名,然后再初始化的参数中给出具体的删除器 typedef void(*funcPtr)(string *); // 等价于using funcPtr = void(*)(string *),其中funcPtr是类型名typename unique_ptr<string, funcPtr> suptr2(new string("I love China!"), myDeleter); // 2)用lambda表达式实现删除器(lambda表达式可以理解为带有operator()的匿名类的对象) unique_ptr<string, decltype(mylambdaDeleter)> suptr3(new string("I love China!"), mylambdaDeleter); // unique_ptr指针和裸指针相同,均占用8bytes string *sptr; int sptrlen = sizeof(sptr); // 8bytes unique_ptr<string> suptr4(new string("I love China!")); int suptrlen = sizeof(suptr4); // 8bytes // 如果增加了自定义的删除器,则unique_ptr的尺寸可能会增加 // 1)lambda表达式实现删除器,不会占用对象内存 int suptr_deleterlen1 = sizeof(suptr3); // 8bytes // 2)在类型模板参数中,写入类型名,然后再初始化的参数中给出具体的删除器,会增加8bytes int suptr_deleterlen2 = sizeof(suptr2); // 16bytes
-
增加字节会对效率有一定影响,所以自定义删除器要慎用。
-
shared_ptr,不会因为自定义删除器而改变尺寸,都是裸指针的2倍。
注意:
-
不要用同一个裸指针初始化多个unique_ptr对象;
-
不要用unique_ptr管理不是new分配的内存;
-
用于函数的参数:
形参:传引用(不能传值,因unique_ptr没有拷贝构造函数)、裸指针。
void func(unique_ptr<T>& up) // 传引用 // void func(unique_ptr<T>* up) // 或者传地址 { ... } int main() { unique_ptr<int> up(new int(10)); func(up); // func(&up); return 0; }
返回unique_ptr:
// 虽然unique_ptr智能指针不能拷贝和赋值,但当这个unique_ptr将要被销毁时,是可以拷贝的(最常见的就是从函数返回一个临时对象) unique_ptr<string> suptrfunc() { unique_ptr<string> tempSUPtr(new string("I love China!")); return tempSUPtr; // 返回这种局部对象,系统会生成一个临时对象,并调用unique_ptr的移动构造函数 // 等价于:return unique_ptr<string>(new string("I love China!")) } // 临时对象构造到suptr14中; unique_ptr<string> suptr1 = suptrfunc(); // 没有unique_ptr智能指针来接收临时对象的话,该临时对象会被释放并销毁其所指向的内存对象 suptrfunc();
-
unique_ptr也可以像普通指针那样,当其作为基类指针指向派生类对象时,也具有多态的性质;
-
unique_ptr不是绝对安全的,如果程序中调用
exit()
全局的unique_ptr可以自动释放,但局部unique_ptr无法释放;
shared_ptr(类模板、强指针):共享式指针
shared_ptr<指向的类型> 智能指针名
,多个指针(使用引用计数机制,来表明资源被几个指针共享)指向同一个对象(共享所有权),直到最后一个指向它的指针被销毁时,这个对象才会被销毁。
初始化:
-
智能指针是explicit(显式类型转换),不能进行隐式类型转换(一般用等号初始化),故要进行直接初始化(即用()圆括号初始化)。
shared_ptr<int> makes(int value) { // return 临时对象 return shared_ptr<int>(new int(value)); // 报错,无法从 new 得到的 int* 转换到 shared_ptr<int> //return new int(value); } shared_ptr<int> iptr2(new int()); shared_ptr<int> iptr3 = makes(0);
-
裸指针可以初始化shared_ptr智能指针,但不推荐裸指针和智能指针穿插使用。
int *iptr4 = new int(); // iptr4,是裸指针 shared_ptr<int> sPtr1(iptr4); // 不推荐使用
-
不支持指针的运算,即+、-、++、–等。
-
不要用shared_ptr管理,不是new分配的内存。
-
make_shared函数:
1)标准库中的函数模板,能够安全、高效的分配和使用shared_ptr;
2)能够动态的在堆中,分配并初始化一个对象,并返回指向该对象的shared_ptr智能指针;
3)使用规范:
shared_ptr<int> sPtr2 = make_shared<int>(100); shared_ptr<string> sPtr3 = make_shared<string>(5, 'a'); // 5个字符a生成的字符串 shared_ptr<string> sPtr4 = make_shared<string>("test"); // sPtr5首先释放指向值为0的内存,然后指向值为100的内存 shared_ptr<int> sPtr5 = make_shared<int>(); // sPtr5指向的是一个int,其中保存的值是0(值初始化) sPtr5 = make_shared<int>(100); // sPtr5指向一个新的int,int中保存的是100 // 使用make_shared初始化时,auto能够自动推断出智能指针类型 auto sPtr6 = make_shared<string>("test"); cout << typeid(sPtr6).name() << endl;
-
移动语义std::move,可以转移对原始指针的控制权。
shared_ptr<int> isptr8(new int(100)); shared_ptr<int> isptr9(std::move(isptr8)); // 移动语义,移动构造一个新的智能指针对象isptr9,仍旧指向原来的内存对象 // isptr8会被置空,所指的内存对象的“强引用计数”仍然是1
1)移动肯定比复制快,复制会增加强引用计数,而移动不会
2)移动构造函数快过拷贝构造函数,移动赋值运算符快过拷贝复制运算符
#include <iostream> #include <memory> using namespace std; int main() { // 10所在的内存空间,被多个shared_ptr指针所指向 shared_ptr<int> p1(new int(10)); shared_ptr<int> p2(p1); // 拷贝构造函数,引用计数加1 shared_ptr<int> p3 = p2; // 拷贝赋值运算符,引用计数加1 // 引用计数: cout << p1.use_count() << endl; // 智能指针指向的内存空间的首地址: cout << p1.get() << endl; cout << p2.use_count() << " : " << p2.get() << endl; cout << p3.use_count() << " : " << p3.get() << endl; return 0; }
-
用函数返回的临时对象来初始化shared_ptr指针。
shared_ptr<int> create(int val) { return make_shared<int>(val); } void myfunc1(int val) { shared_ptr<int> tempSPtr = create(val); return; // 离开作用域后,tempSPtr会被自动释放,它所指向的内存也会被释放 } shared_ptr<int> myfunc2(int val) { shared_ptr<int> tempSPtr = create(val); return tempSPtr; // 系统根据tempSPtr产生一个临时shared_ptr对象,并返回 } // shared_ptr的使用场景: void func() { // 如果没有shared_ptr变量来接收myfunc2(10)返回的临时对象的话,那么返回的临时shared_ptr对象会被销毁 // myfunc2(10); // 用shared_ptr变量接受返回的临时shared_ptr对象 shared_ptr<int> isptr1 = myfunc2(10); }
shared_ptr的底层结构:
-
共享式,每个shared_ptr的拷贝都指向相同的内存;
-
引用计数(又称强引用计数
strong refs
),会记录指向该对象内存的指针个数;只有当最后一个指向该内存(对象)的shared_ptr指针不再需要指向该对象时,即引用计数为0时shared_ptr指针才会析构所指向的内存对象。注意:弱引用计数,一般会和
weak_ptr
搭配使用,解决交叉引用的问题。- 弱引用计数
weak refs
,不会改变所指向内存对象的生命周期,只有强引用才可以改变生命周期。 - weak_ptr绑定到shared_ptr上, 不会改变shared_ptr所指向的内存对象的强引用计数,但会改变弱引用计数。
- 弱引用计数
-
自定义的删除器:在创建一个数组并用智能指针指向它(需要通过删除器,调用delete[]来析构,因默认采用delete直接析构) / 在线程池队列中拿到一个线程并封装为一个智能指针(归还线程时,会重新添加到队列中,而不是直接析构)时,需要自定义或使用默认删除器
default_delete
。
shared_ptr工作机制的分析:
引用计数的增加:
1)sPtr2和sPtr指向相同的对象,该对象目前有两个引用者;
auto sPtr1 = make_shared<int>(100);
auto sPtr2(sPtr1); // sPtr2和sPtr指向相同的对象
2)把智能指针当作实参往函数里进行“值传递”(如果进行“引用传递”,则引用计数不会增加);
// 值传递(如果参数为引用,则智能指针的引用次数不会增加)
void testfunc1(shared_ptr<int> tmpPtr)
{
return;
}
// 把智能指针当作实参往函数里进行“值传递”(如果进行“引用传递”,则引用计数不会增加);
testfunc1(sPtr1);
3)作为函数的返回值;
shared_ptr<int> testfunc2(shared_ptr<int>& tmpPtr)
{
return tmpPtr;
}
// 作为函数的返回值,并被接收,则会增加引用计数
shared_ptr<int> sPtr3 = testfunc2(sPtr1);
// 如果没有变量来接受这个临时的智能指针,则当临时智能指针生命周期到了会自动恢复调用函数前的引用次数
testfunc2(sPtr1);
引用计数的减少:
1)给shared_ptr赋予新值,让该shared_ptr指向一个新的对象;
2)局部的shared_ptr离开其作用域,即进入函数体后计数加一,离开函数体后计数减一(恢复到未调用函数前);
3)当一个shared_ptr引用计数变为0,则会自动释放所指向的对象内存;
支持普通的拷贝和赋值:
左值的shared_ptr的计数器将减1,右值的shared_ptr的计数器将加1。
shared_ptr<int> pa1(new int (10));
shared_ptr<int> pa2 = pa1;
shared_ptr<int> pb1(new int (11));
shared_ptr<int> pb2 = pb1;
pb1 = pa2; // 10所在的内存区域的引用计数为3
// 11所在的内存区域的引用计数为1
pb2 = pa2; // 10所在的内存区域的引用计数为4
// 11所在的内存区域的引用计数为0,故会自动析构掉
shared_ptr智能指针常用操作:
-
use_count()
:返回多少个shared_ptr指针指向某个对象,即引用计数器的值。shared_ptr<int> sPtr1 = make_shared<int>(100); shared_ptr<int> sPtr2(sPtr1); shared_ptr<int> sPtr3; sPtr3 = sPtr2; int iCount = sPtr1.use_count(); cout << iCount << endl;
-
unique()
:是否该智能指针独占某个指向的对象(即use_count()返回值为一,是独享则返回true)。sPtr4 = make_shared<int>(300); cout << sPtr4.unique() << end;
-
reset()
:恢复(复位/重置)的意思,即改变与资源的关联关系。// reset不带参数时,执行复位操作: // 1)若智能指针是唯一指向该对象的指针,那么释放该指针所指向的对象并将其置空。 shared_ptr<string> sPtr4(new string(5, 'a')); sPtr4.reset(); if (sPtr4 == nullptr) { cout << "sPtr4已经被置空" << endl; } // 2)若智能指针不是唯一指向该对象的指针,则不释放该指针所指向的对象,但该对象的引用计数会减1;同时让该指针置空。 sPtr4 = make_shared<string>(5, 'a'); shared_ptr<string> sPtr5 = sPtr4; sPtr5.reset(); if (sPtr5 == nullptr) { cout << "sPtr5已经被置空" << endl; } // reset带参数(一般是一个new出来的指针)时,执行重置操作: // 1)若智能指针是唯一指向该对象的指针,则释放该内存对象并让智能指针指向新的内存对象。 sPtr4.reset(new string("test4")); // 2)若智能指针不是唯一指向该对象的指针,则不释放该内存对象;但指向该对象的引用计数会减少,同时让智能指针指向新的内存对象。 shared_ptr<string> sPtr6 = sPtr4; sPtr6.reset(new string("test6")); if (sPtr6.unique() & sPtr4.unique()) { cout << "sPtr4, sPtr6分别独占某个指向的对象" << endl; } // 3)空指针也可以用reset来重新初始化。 shared_ptr<int> ptr; ptr.reset(new int(1));
-
*
解引用:获取智能指针指向的对象,如cout << *ptr << endl;
。 -
get()
:返回裸指针,主要目的:有些函数(第三方函数)的参数,需要的是一个内置的裸指针而不是智能指针。shared_ptr<int> myIPtr = make_shared<int>(10); int* p = myIPtr.get(); // 返回ptr中保存的指针(裸指针) *p = 20; //delete p; // 报错,系统报错,因为该指针由智能指针来管理,只有当智能指针释放了所指的对象,该指针才变为无效 myIPtr.reset(); p = nullptr;
注意:不要用get()得到的裸指针,来初始化另一个智能指针或者给另一个智能指针赋值。
-
swap()
:交换两个智能指针所指向的对象shared_ptr<string> mySPtr1 = make_shared<string>("test"); shared_ptr<string> mySPtr2 = make_shared<string>(5, 'a'); cout << "交换前:" << "mySPtr1 = " << *mySPtr1 << "\t" << "mySPtr2 = " << *mySPtr2 << endl; std::swap(mySPtr1, mySPtr2); cout << "交换后:" << "mySPtr1 = " << *mySPtr1 << "\t" << "mySPtr2 = " << *mySPtr2 << endl;
-
= nullptr
:将所指向的对象,引用计数减一(若该对象的引用计数变为0,则释放智能指针所指向的对象);并将智能指针置空。shared_ptr<string> Sptr1(new string("test")); shared_ptr<string> Sptr2 = Sptr1; Sptr1 = nullptr; if (Sptr1) // 可以用智能指针的名字作为判断条件 { cout << "Sptr1指向内存对象,不为空" << endl; } else { cout << "Sptr1为空" << endl; }
shared_ptr提供了支持数组的特化版本:
- 数组版本的shared_ptr重载了操作符[],且操作符返回的是引用,可作为左值使用。
指定删除器:
指定删除器以及数组问题:
-
通过
指定删除器
取代系统提供的默认删除器(delete运算符作为默认的资源析构方式),当智能指针要删除所指向的对象时编译器会调用我们自定义的删除器。// 自定义的删除器,要在所指向的对象的引用计数为0时,删除该对象 void myDelete(int *ptr) { delete ptr; // 写日志 cout << ".....调用了自动定义的删除器......" << endl; } shared_ptr<int> iptr1(new int(123), myDelete); // myDelete为自定义的删除器函数 shared_ptr<int> iptr2(iptr1); iptr2.reset(); // 调用默认的删除方式,使指针iptr2 = nullptr,其所指向的对象的引用次数变为1 iptr1.reset(); // 所指向的引用次数为1,故要释放智能指针所指向的对象,会调用自定义的删除器,同时置空该智能指针
-
有些情况编译器处理不了(用shared_ptr管理动态数组),需要提供自定义的删除器。
void myDeleteObjArr(A *ptr) { delete[] ptr; } //shared_ptr<A> objPtr1(new A[3]()); //delete objPtr1; // 报错,因为系统释放objPtr是delete objPtr所指向的裸指针,而不是delete[] objPtr shared_ptr<A> objPtr2(new A[3](), myDeleteObjArr); objPtr2.reset();
-
也可以用
default_delete
来做删除器,default_delete
是标准库中的类模板。// 未封装前: shared_ptr<A> objPtr3(new A[3](), std::default_delete<A[]>()); objPtr3.reset(); // 用一个函数模板来封装shared_ptr数组,并使用default_delete删除器 template<typename T> shared_ptr<T> make_shared_arrPtr(size_t size) { return shared_ptr<T>(new T[size](), default_delete<T[]>()); } shared_ptr<int> arrPtr2 = make_shared_arrPtr<int>(5); arrPtr2.reset();
-
定义数组时,在尖括号中加
[]
。// 在尖括号中加[ ] shared_ptr<A[]> objPtr4(new A[3]); objPtr4.reset(); shared_ptr<int[]> arrPtr1(new int[3]); arrPtr1[0] = 10; arrPtr1[1] = 11; arrPtr1[2] = 12; arrPtr1.reset();
指定删除器额外说明:
-
就算两个shared_ptr制定了不同的删除器,只要它们所指的对象类型相同,那么这两个shared_ptr就属于相同的类型。
auto lambda1 = [](int* ptr) // 可以通过一个lambda表达式自定义删除器 { cout << "调用了自定义的lambda表达式, auto lambda1 = [](int* ptr) 作为删除器" << endl; //日志 delete ptr; }; auto lambda2 = [](int* ptr) { cout << "调用了自定义的lambda表达式, auto lambda2 = [](int* ptr) 作为删除器" << endl; //日志 delete ptr; }; shared_ptr<int> ptr_i1(new int(100), lambda1); shared_ptr<int> ptr_i2(new int(200), lambda2); // ptr_i2会先调用lambda2把自己所指的对象释放;然后指向ptr_i1所指的对象,此时ptr_i1所指的对象的引用计数为2 // 整个main执行完后,会调用lambda1来释放ptr_i1和ptr_i2共同指向的对象 ptr_i2 = ptr_i1;
-
由于ptr_i1、ptr_i2类型相同,故可以放在元素类型为该对象类型的同一容器中。
vector<shared_ptr<int>> ptrVector; ptrVector.push_back(ptr_i1); ptrVector.push_back(ptr_i2);
总结:make_shared是编译器提倡的生成shared_ptr智能指针的方法,但这种方法无法指定自己的删除器。
shared_ptr在多线程环境中的安全性:
-
在多线程环境中,引用计数本身是线程安全的(引用计数是原子操作),但shared_ptr本身并不是线程安全的;
存在问题:对于shared_ptr,多线程并发访问可能会导致竞争条件和数据不一致的问题。
-
多线程同时读同一个shared_ptr对象是线程安全的;如果多个线程对同一个shared_ptr对象同时写,则需要加锁;
#include <iostream> #include <memory> #include <shared_mutex> #include <thread> using namespace std; // c++14引入: shared_mutex s_mtx; shared_ptr<int> shared_ptr = std::make_shared<int>(42); /* 使用shared_mutex / shared_lock来确保线程安全,这些工具可以提供“共享的读锁,互斥的写锁”的功能 */ void read_shared_ptr() { shared_lock<shared_mutex> lock(s_mtx); cout << "Shared pointer value: " << *shared_ptr << endl; } void write_shared_ptr() { unique_lock<shared_mutex> lock(s_mtx); *shared_ptr = 100; cout << "Shared pointer value updated: " << *shared_ptr << endl; } int main() { thread t1(read_shared_ptr); thread t2(write_shared_ptr); t1.join(); t2.join(); return 0; } // 这样,多个线程可以同时读取共享指针的值,但只有一个线程能够修改它。
自定义简化的shared_ptr:
#include <iostream>
using namespace std;
namespace _nmsp
{
class A
{
public :
A() { cout << "default constructor" << endl; }
~A() { cout << "destructor" << endl; }
public:
int x, y;
};
class shared_ptr
{
public:
shared_ptr() : obj(nullptr), cnt(nullptr) {}
shared_ptr(A*);
shared_ptr(const shared_ptr&);
shared_ptr& operator=(const shared_ptr&);
int use_count() { return (*this->cnt); }
A *operator->() { return obj; }
A &operator*() { return *obj; }
~shared_ptr();
private:
int* cnt;
A* obj;
};
shared_ptr::shared_ptr(A* _obj) : obj(_obj)
{
cout << "shared_ptr::shared_ptr(A* obj)" << endl;
// 考虑到可能会有nullptr,隐式转换为A*后,调用该有参(转换)构造函数
//,故这里加上了判断条件
if (_obj == nullptr) {
this->cnt = nullptr;
} else {
this->cnt = new int(1);
}
}
shared_ptr::~shared_ptr()
{
if (this->cnt != nullptr)
{
(*this->cnt) -= 1;
if ((*this->cnt) == 0)
{
delete this->obj;
this->obj = nullptr;
}
}
}
shared_ptr::shared_ptr(const shared_ptr& ptr) : obj(ptr.obj), cnt(ptr.cnt)
{
cout << "shared_ptr::shared_ptr(const shared_ptr& ptr)" << endl;
// 考虑传入的ptr可能是{ ptr.cnt == nullptr, ptr.obj == nullptr }
if (this->cnt != nullptr)
{
(*this->cnt) += 1;
}
}
shared_ptr& shared_ptr::operator=(const shared_ptr& ptr)
{
cout << "shared_ptr& shared_ptr::operator=(const shared_ptr& ptr)" << endl;
// 避免“自我赋值”
if (this->obj == ptr.obj)
{
return *this;
}
if (this->cnt != nullptr)
{
(*this->cnt) -= 1;
if ((*this->cnt) == 0)
{
delete this->obj;
this->obj = nullptr;
this->cnt = nullptr;
}
}
this->obj = ptr.obj;
this->cnt = ptr.cnt;
// 考虑传入的ptr可能是{ ptr.cnt == nullptr, ptr.obj == nullptr }
if (ptr.cnt != nullptr)
{
(*this->cnt) += 1;
}
return *this;
}
}
int main()
{
_nmsp::shared_ptr p1(new _nmsp::A());
cout << p1.use_count() << endl; // 1
_nmsp::shared_ptr p2 = p1;
cout << p2.use_count() << endl; // 2
cout << p1.use_count() << endl; // 2
// 先调用有参(转换)构造函数,再调用拷贝赋值运算符:
p1 = nullptr; // p1会(在赋值运算符调用过程中)自动析构
// 此时,p1{ cnt == nullptr, obj == nullptr }
cout << p2.use_count() << endl; // 1
// 此时,p1{ cnt == nullptr, obj == nullptr },p2{ *cnt == 1, obj != nullptr }
p1 = p2;
// 此时,p1/p2{ *cnt == 2, obj != nullptr }
cout << p2.use_count() << endl;
cout << p1.use_count() << endl;
return 0;
}
weak_ptr(类模板、弱指针):用来辅助shared_ptr工作
shared_ptr 相互引用时的死锁问题(存在循环引用):
- 使两个指针的引用计数永远不可能下降为 0,会导致堆中的内存无法正常回收,从而造成内存泄露。
-
弱引用的智能指针weak_ptr(不会修改引用计数的值,只会增加弱引用计数),故会打破这种循环引用,且可以检测到它所管理的对象是否已经被释放。
-
为了避免出现循环引用的问题,可以使用weak_ptr和shared_ptr共同作用,即用一种观察者模式。
class B; class A { public: ~A() { cout<<"A delete\n"; } public: // weak_ptr<B> pb_; // 采用weak_ptr,解决循环引用导致引用计数无法降为0的问题 shared_ptr<B> pb_; }; class B { public: ~B() { cout<<"B delete\n"; } public: // weak_ptr<A> pa_; // 采用weak_ptr,解决循环引用导致引用计数无法降为0的问题 shared_ptr<A> pa_; }; void fun() { shared_ptr<B> pb(new B()); shared_ptr<A> pa(new A()); pb->pa_ = pa; pa->pb_ = pb; cout << pb.use_count() << endl; cout << pa.use_count() << endl; } int main() { fun(); return 0; }
weak_ptr的底层结构(与shared_ptr相同):
和shared_ptr尺寸是相同的,是裸指针的2倍。
内存对象、控制块(强引用计数strong refs、弱引用计数weak_refs、其他数据(自定义删除器、分配器))是由shared_ptr强指针创建的,但weak_ptr也是指向该内存对象和控制块的。
weak_ptr的创建:
shared_ptr需要释放所指向的内存对象时,不需要考虑weak_ptr是否指向该内存对象。
- weak_ptr指向的是由shared_ptr所管理的内存对象,但weak_ptr不控制所指向内存对象的生命周期。
- 弱引用计数,不会改变所指向内存对象的生命周期,只有强引用才可以改变生命周期。
weak_ptr绑定到shared_ptr上, 不会改变shared_ptr所指向的内存对象的引用计数(又称强引用计数strong refs
),但会改变弱引用计数(weak refs
)。
// weak_ptr的创建:
// 强指针isptr1
shared_ptr<int> isptr1 = make_shared<int>(10);
// iwptr弱共享isptr,isptr所指向的内存对象的引用计数(强引用计数)不变,但弱引用计数会加一
weak_ptr<int> iwptr1(isptr1);
weak_ptr<int> iwptr2(iwptr1);
// iwptr1、iwptr2是两个弱指针
// 此时,strong refs : 1、weak refs : 2
弱引用(weak_ptr)的作用是:
- 监视shared_ptr(强引用)的生命周期,是一种对shared_ptr的扩充;
- 非独立的智能指针,不能用来操作所指向的内存对象,故称为 “助手/旁观者”,weak_ptr能够判断所指向的内存对象是否存在。
weak_ptr常规操作:
-
use_count()
:该内存对象的“强引用计数”,即与该弱指针共享对象的其他share_ptr
的数量。shared_ptr<int> isptr3 = make_shared<int>(100); auto isptr4(isptr3); weak_ptr<int> iwptr2(isptr3); // 定义弱指针 int iscount = isptr3.use_count(); int iwcount = iwptr2.use_count(); // 返回指向该内存对象的强引用的次数 cout << "iscount = " << iscount << "\t" << "iwcount = " << iwcount << endl;
-
expired()
:是否过期。弱指针所指向的对象的use_count()==0
,表示其所指向的对象不存在了,则返回true,否则返回false。即用来判断所观测shared_ptr强指针所指向的内存对象,是否已经被释放了。shared_ptr<int> isptr3 = make_shared<int>(100); auto isptr4(isptr3); weak_ptr<int> iwptr2(isptr3); isptr3.reset(); isptr4.reset(); if (iwptr2.expired()) { cout << "弱指针iwptr2所观测的强指针所指向的内存对象,已经被释放了" << endl; }
-
reset()
:将该弱引用指针置空,不影响所指向内存对象的强引用计数,但所指向内存对象的“弱引用计数”会减少。#include <iostream> #include <memory> using namespace std; int main() { shared_ptr<int> isptr3 = make_shared<int>(100); auto isptr4(isptr3); weak_ptr<int> iwptr2(isptr3); iwptr2.reset(); cout << isptr3.use_count() << " " << iwptr2.use_count() << endl; // 2 0 return 0; }
-
lock()
:先检查weak_ptr所指向的内存对象是否存在,
1)存在,则返回一个该内存对象的shared_ptr指针,同时强引用计数加一;(作用:通过lock()返回shared_ptr,且这种行为是线程安全的)
2)不存在,则返回一个空的shared_ptr指针;auto isptr5 = make_shared<int>(2); weak_ptr<int> iwptr3(isptr5); // 可以用shared_ptr给weak_ptr赋值 if (!iwptr3.expired()) // 等价于:iwptr3.use_count() != 0 { auto isptr6 = iwptr3.lock(); // 返回的isptr6是强指针,此时强引用计数会加一 if (isptr6 != nullptr) { cout << typeid(isptr6).name() << endl; } } else { cout << "弱指针iwptr3所观测的强指针所指向的内存对象,已经被释放了" << endl; } // 程序运行完判断语句后,强引用次数又恢复原来的值,即{}会影响强指针的作用域
-
operator=():把
shared_ptr / weak_ptr
赋值给weak_ptr。 -
swap():交换。
-
weak_ptr没有
重载->操作符
和重载*操作符
,故不能直接访问资源,需借助lock()
返回的shared_ptr才能访问指向的内存块资源。#include <iostream> #include <string> #include <memory> using namespace std; class B; class A { public: A() { cout << "A()" << endl; } A(string str) : m_str(str) { cout << "A(string str)" << endl; } ~A() { cout << "~A()" << endl; } string m_str; weak_ptr<B> m_wptr; // 防止出现循环引用,故采用“弱指针” }; class B { public: B() { cout << "B()" << endl; } B(string str) : m_str(str) { cout << "B(string str)" << endl; } ~B() { cout << "~B()" << endl; } string m_str; weak_ptr<A> m_wptr; }; int main() { shared_ptr<A> sptr1(new A("lili")); { shared_ptr<B> sptr2(new B("yoyo")); sptr1->m_wptr = sptr2; sptr2->m_wptr = sptr1; /*if (sptr1->m_wptr.expired()) { cout << "sptr1->m_wptr已销毁" << endl; } else { // 执行这里时可能是线程不安全的:资源可能在其他线程已经被释放,故m_str的访问可能导致程序崩溃 cout << "sptr1->m_wptr.lock()->m_str = " << sptr1->m_wptr.lock()->m_str << endl; }*/ // 线程安全的做法: shared_ptr<B> sptr3 = sptr1->m_wptr.lock // 通过lock()返回shared_ptr的行为是线程安全的 if (sptr3 == nullptr) { cout << "sptr1->m_wptr已销毁" << endl; } else { cout << "sptr3->m_str = " << sptr3->m_str << endl; } } /*if (sptr1->m_wptr.expired()) { cout << "sptr1->m_wptr已销毁" << endl; } else { // 执行这里时可能是线程不安全的:资源可能在其他线程已经被释放,故m_str的访问可能导致程序崩溃 cout << "sptr1->m_wptr.lock()->m_str = " << sptr1->m_wptr.lock()->m_str << endl; }*/ // 线程安全的做法: shared_ptr<B> sptr3 = sptr1->m_wptr.lock(); // 通过lock()返回shared_ptr的行为是线程安全的 if (sptr3 == nullptr) { cout << "sptr1->m_wptr已销毁" << endl; } else { cout << "sptr3->m_str = " << sptr3->m_str << endl; } return 0; }
智能指针的删除器:
默认情况下,智能指针过期时,用delete原始指针,释放它管理的资源;也可以自定义删除器,改变智能指针释放资源的行为;
删除器,可以是全局函数、仿函数、lambda表达式,形参:原始指针。
- shared_ptr:就算两个shared_ptr指定的删除器不同,只要他们所指向的内存对象相同,那么这两个shared_ptr也属于同一个类型(即可以装在同一个容器中)。
- unique_ptr:指定unique_ptr中的删除器不同,会导致unique_ptr的类型不同(因为删除器类型名,直接被定义在类模板参数中了)(故不能放在同一个容器中)。
总结:shared_ptr指定的删除器,更加灵活,且不作为类型判断标准。
#include <iostream>
#include <memory>
using namespace std;
// 全局函数:
void deletefunc(int* ptr)
{
delete ptr;
ptr = nullptr;
cout << "void deletefunc(int* ptr)" << endl;
}
// 仿函数:重载operator()的类
struct Deleteclass
{
void operator()(int* ptr)
{
delete ptr;
ptr = nullptr;
cout << "void operator()(int* ptr)" << endl;
}
};
// lambda表达式:
auto deletelambda = [](int* ptr) {
delete ptr;
ptr = nullptr;
cout << "deletelambda" << endl;
};
int main()
{
// 给share_ptr指定删除器:
shared_ptr<int> sptr1(new int(10), deletefunc);
shared_ptr<int> sptr2(new int(10), Deleteclass());
shared_ptr<int> sptr3(new int(10), deletelambda);
// 给unique_ptr指定删除器:
unique_ptr<int, decltype(deletefunc)*> sptr4(new int(10), deletefunc);
unique_ptr<int, void(*)(int*)> sptr5(new int(10), deletefunc);
unique_ptr<int, Deleteclass> sptr6(new int(10), Deleteclass());
unique_ptr<int, decltype(deletelambda)> sptr7(new int(10), deletelambda);
return 0;
}
智能指针的主要目的:
帮助我们自动释放内存,防止忘记释放内存而引发内存泄漏,多用于多线程的场景。
智能指针的选择:
如果unique_ptr能解决问题,则不需要使用shared_ptr,因unique_ptr效率更高且占用资源少。
- 如果程序中,使用多个指向同一个内存对象的指针,要选择shared_ptr,一般用在多线程场景中;
- 如果程序中,只需要一个指针指向该对象,则选择unique_ptr;
总的来说,智能指针shared_ptr和weak_ptr主要用于多线程程序中,故一定要考虑“线程安全”的问题;
-
在多线程环境中,引用计数本身是线程安全的(引用计数是原子操作),但shared_ptr本身并不是线程安全的;
存在问题:对于shared_ptr,多线程并发访问可能会导致竞争条件和数据不一致的问题。
-
多线程同时读同一个shared_ptr对象是线程安全的;如果多个线程对同一个shared_ptr对象同时写,则需要加锁。
使用
shared_mutex / shared_lock / unique_lock
来确保线程安全,这可以提供“共享的读锁,互斥的写锁”的功能。
引用计数:
shared_ptr智能指针:通过引用计数确保内存块的生命周期。
string:
-
eager-copy
存储方式,每次都会重新申请新的内存空间,存放string -
copy-on-write
存储方式,即拷贝构造时两个字符串指向同一个内存空间,当某一个修改时,则重新申请空间并将内容写入该空间。string的copy-on-write存储方式,简单实现:
#include <iostream> #include <cstring> using namespace std; class String { public: // 调用构造函数生成对象时,会创建一个内存块,包含“字符串”和“引用计数” explicit String(const char* chArr_ = "") : ePString(new EPString(chArr_)) { //cout << "explicit String::String(const char* chArr_ = "")" << endl; } // 调用拷贝构造函数生成对象时,两个对象指向同一块内存空间,只需将引用计数加一即可 String(const String& str_) : ePString(str_.ePString) { if (this->ePString->is_shareable == true) { ++(this->ePString->refCount); } else { this->ePString = new EPString(str_.ePString->str); } //cout << "String::String(const String& str_)" << endl; } // 调用拷贝赋值运算符生成对象时,先将指向的源内存块的引用计数减一 //,之后指向新的内存块并将新内存块的引用计数值加一 String& operator=(const String& str_) { // 避免“自我赋值” if (this->ePString == str_.ePString) { return *this; } --(this->ePString->refCount); if (this->ePString->refCount == 0) // 源内存块的引用计数为0时,则释放该内存块 { delete this->ePString; } this->ePString = str_.ePString; ++(this->ePString->refCount); //cout << "String& String::operator=(const String& str_)" << endl; return *this; } /* // 该版本的operator[]存在的原因:为了防止const对象无法调用而报错(const对象只能调用const成员函数) const char& operator[](int pos) const { cout << "const char& operator[](int pos) const" << endl; return ePString->str[pos]; }*/ // 两种版本同时存在时,会直接调用非const版本 char& operator[](int pos) { //cout << "char& operator[](int pos)" << endl; // 此时该对象所指向内存块的的引用计数超过1时,表示还有其他对象指向该内存块 if (ePString->refCount > 1) { --(this->ePString->refCount); this->ePString = new EPString(ePString->str); // “写时复制” } //一旦调用operator[]运算符,则认为可能会修改该内存块,故不能在共享该内存块,即is_shareable=false this->ePString->is_shareable = false; // 此时可直接返回pos位置的引用,并在外部进行修改 return this->ePString->str[pos]; } // 返回该对象指向的内存块的引用计数: int use_count() const { return ePString->refCount; } void GetString() const { for (int i = 0; i < strlen(ePString->str); ++i) { cout << ePString->str[i]; } cout << endl; } ~String() { --(ePString->refCount); if (ePString->refCount != 0) { return; } delete this->ePString; //cout << "String::~String()" << endl; } private: class EPString { public: int refCount; // 该内存块的引用计数 char* str; // 内存块中,保存的字符串 bool is_shareable; // 该内存块是否能被共享 public: EPString(const char* chArr_) : refCount(1), is_shareable(true) { str = new char[strlen(chArr_) + 1]; strcpy(str, chArr_); //cout << "EPString(const char* chArr_)" << endl; } ~EPString() { delete[] str; //cout << "EPString::~EPString()" << endl; } }; private: EPString* ePString; }; int main() { String str1("hello"); cout << str1.use_count() << endl; String str2(str1); // 调用拷贝构造函数 cout << str2.use_count() << endl; String str3; cout << str3.use_count() << endl; String str4 = str3; // 调用拷贝构造函数 cout << str4.use_count() << endl; str3 = str2; // 调用拷贝赋值运算符 cout << str3.use_count() << endl; cout << "..........................." << endl; // copy-on-write写时复制: str3[0] = 'H'; str3.GetString(); cout << "..........................." << endl; cout << str1.use_count() << endl; cout << str2.use_count() << endl; cout << str3.use_count() << endl; cout << str4.use_count() << endl; cout << "..........................." << endl; String str5("Hello"); char* chPtr = &str5[0]; // 由于这里str5调用了operator[],故在内存块不能再被共享 String str6 = str5; // 用于该str5指向的内存块不能再被共享,故在调用拷贝构造函数时,需要重新申请新的内存块 *chPtr = 'h'; cout << str5.use_count() << endl; cout << str6.use_count() << endl; str5.GetString(); str6.GetString(); return 0; }
- 当调用构造函数生成两个string对象时,指向不同的内存空间;
- 当调用拷贝构造函数时,指向相同的内存空间且引用计数加一;
- 当调用拷贝赋值运算符时,指向的源内存块引用计数减一,之后指向新的内存块且引用计数加一;
- 引入
is_shareable
,为false则表示该内存块不可再被共享,之后再调用拷贝构造则需要重新申请内存空间。
-
写时复制copy-on-write、外部加锁、内部加锁。
外部加锁:由调用者负责加锁,决定“跨线程”使用“共享内存”时,加锁的时机。
内部加锁(不常见):对象将所有对自己的访问串行化(通过为每个成员函数加锁实现),故不再需要在多线程中共享该对象时进行外部加锁。
-
copy-on-write
的使用场景/代价:使用场景:当字符串重复次数越多,内存节省的效果越明显,否则copy-on-write的设计则没有必要。
代价:引入了
EPString
这中内部的辅助类,故会额外的占用内存空间;在String类实现过程中,额外的增加的代码,也提高了程序的复杂度。
c++的多态:
引入:
(不考虑多态时)父类指针指向子类对象、子类指针指向父类对象:
- 父类指针指向子类对象,那么经由该指针只能访问基础类定义的函数(静态编译后,还没考虑动态多态);
- 子类指针指向父类对象,必须经过强制转换,但存在风险(调用可能存在部分成员变量未定义的问题)!!!
- 若父类和子类定义了同名的成员函数,则通过对象指针调用成员函数时,具体调用哪个函数要根据指针的类型来确定,而非指针实际指向的对象类型。
虚函数的,就是为了避免指向子类对象的父类指针,只能调用父类的成员函数,而设计的。
- 主要目的:当(含有纯虚函数的)抽象父类无法被实例化,即可通过其指针操纵各个子类。
主要分为静态多态和动态多态:
静态多态:编译时就已确定要执行函数的入口地址,主要有:函数重载、函数模板、类模板。
动态多态:通过虚函数机制实现,即在运行期间动态绑定(若子类没有重写父类中的虚函数,则多态是一件毫无意义的事情)。
- 运行时,确认对象类型、定位要调用的函数。
- 主要用于:基类指针指向派生类对象/引用派生类对象时,调用派生类中重写(函数特征相同)的基类虚成员函数。
- 缺点:需要通过“虚函数表指针 --> 虚函数表 --> 虚函数地址”定位,故性能有所下降。
多态的实现方式:
动态多态:
#include <iostream>
using namespace std;
const double Pi = 3.14;
// 抽象基类
class Shape
{
public:
virtual double Area() const = 0; // 含有纯虚函数的类为抽象类,不能产生对象
void Display() { cout << Area() << endl; }
};
class Rectangle : public Shape
{
public:
Rectangle(const double& m_width, const double& m_height) : width(m_width), height(m_height) { }
virtual double Area() const { return width * height; }
private:
double width;
double height;
};
class Circle : public Shape
{
public:
Circle(const double& m_radius) : radius(m_radius) { }
virtual double Area() const { return Pi * radius * radius; }
private:
double radius;
};
// 多态性的体现:
int main()
{
Rectangle rectangle(2.0, 3.0);
Circle circle(2.0);
// 重要的两点:父类指针指向了子类对象、子类重写了父类的纯虚函数
Shape* shape[2] = {&rectangle, &circle};
shape[0]->Display();
shape[1]->Display();
return 0;
}
通过继承、虚函数(“父类指针/父类引用”绑定子类对象),可以实现运行时多态:
- 继承实现后,可以将基类看作子类的一个特殊成员。
- 如果没有继承语法,可以通过组合实现,即在子类中定义基类对象。
访问权限:
- public/private:用来控制外部对成员变量的访问权限
- protected:用来控制子类对基类的访问权限
虚函数:
-
虚函数表,可认为是子类隐藏的一个成员数组(数组中标注每个虚函数的入口地址)。
编译器和运行时环境,通过虚函数表,保证调用正确版本的函数。
-
父类成员函数前加virtual,即允许子类重写基类的虚函数,并通过重写实现运行时多态。
若基类定义了纯虚函数,则要求子类必须重写该函数。
-
final修饰父类的虚函数,则要求子类不能重写该虚函数。
-
override修饰子类的虚函数,则表明该虚函数是重写父类的。
运行时多态:
#include <iostream>
using namespace std;
class A
{
public:
virtual void Display()
{
cout << "A" << endl;
}
};
class B : public A
{
public:
virtual void Display()
{
cout << "B" << endl;
}
};
int main()
{
B b;
// 基类指针指向派生类对象,并调用派生类中重写了的基类成员函数
A* a1 = &b; a1->Display();
// 基类引用派生类对象,并调用派生类中重写了的基类成员函数
A& a2 = b; a2.Display();
return 0;
}
如果不用指针/引用而直接使用一个对象,可能导致意外的切片行为(即从子类转换到基类时,丢失了子类的数据)。
#include <iostream>
using namespace std;
class A
{
public:
virtual void foo()
{
cout << "A's foo()" << endl;
bar();
}
virtual void bar()
{
cout << "A's bar()" << endl;
}
};
class B: public A
{
public:
void foo() override
{
cout << "B's foo()" << endl;
A::foo();
}
void bar() override
{
cout << "B's bar()" << endl;
}
};
int main()
{
B bobj;
// 基类A指针指向派生类B对象:
A *aptr = &bobj; aptr->foo();
// 转化为派生类B对象为基类A对象:
A aobj = *aptr;
aobj.foo();
return 0;
}
/*
// 多态性的影响,aptr->foo()输出结果是:
B's foo() // 多态性,执行了子类B重载父类A的虚函数
A's foo() // 执行了`A::foo();`
B's bar() // 虽然调用的是这个函数`A::foo()`,但隐式传入的还是 bobj 的地址(bar()的参数列表中隐含了一个this指针),所以调用bar()时还是会调用B的函数
// 与多态无关,aobj.foo()输出结果是:
A's foo() // 这个不是指针,aobj完全是一个A的对象
A's bar()
*/
静态多态:
模板、模板类:
-
对于两个只有参数类型不同的函数不用重复编写,编译器会自动生成程序中用到的不同类型的函数,但这也会导致模板较多的程序编译起来非常慢;
c++的STL中使用了大量的模板,std::vector可以用来保存任意类型,std::pair可以由任意两个类型组成。
-
模板类的函数声明和定义需要放在一起,原因:
- 模板代码生成过程,从编译器角度讲,模板函数本身并不是一个能直接拿来链接的函数,而是需要用它来生成一些其他的函数;
- 将函数声明和定义拆开编写,其实是在链接阶段再去处理函数名称和函数实现间的绑定,而链接器通常没办法,在链接阶段,再去处理模板参数的替换;
通过模板,可以实现编译时/静态多态。
#include <iostream>
using namespace std;
const double Pi = 3.14;
template <typename T>
class Rectangle
{
public:
typedef T value_type; // 参数类型
typedef T retVal_type; // 返回值类型
public:
Rectangle(const T& m_width, const T& m_height) : width(m_width), height(m_height) { }
T Area() const { return width * height; }
private:
T width;
T height;
};
template <typename T>
class Circle
{
public:
typedef T value_type;
typedef double retVal_type;
public:
Circle(const T& m_radius) : radius(m_radius) { }
T Area() const { return Pi * radius * radius; }
private:
T radius;
};
// 函数后置类型,推导
template <typename T>
auto Area1(const T& t) -> decltype(t.Area())
{
return t.Area();
}
// 这里需要使用 typename T::retVal_type 的声明 T::retVal_type 是一个类型
//,而非静态变量/静态成员函数(非静态成员变量和非静态成员函数不能这样访问)
template <class T>
typename T::retVal_type Area2(const T& t)
{
return t.Area();
}
int main()
{
Rectangle<double> rectangle(2.0, 3.0);
Circle<double> circle(2.0);
// 多态性的体现:
cout << Area1(rectangle) << "," << Area1(circle) << endl;
cout << Area2(rectangle) << "," << Area2(circle) << endl;
return 0;
}
基类调用子类的接口,即(静态)多态的体现:
#include <iostream>
using namespace std;
const double Pi = 3.14;
template <class T>
class Shape
{
public:
T& toChild() { return static_cast<T&>(*this); }
/* 这里Area函数体内部调用的是,子类的Area函数,即派生类为基类提供接口 */
void Area() { cout << toChild().Area() << endl; }
private:
friend T; // 定义派生类T为基类Shape的友元类
Shape() {} // 私有默认构造函数,即Shape类不能定义0参对象
};
template <typename T>
class Rectangle : public Shape<Rectangle<T>>
{
public:
typedef T value_type; // 参数类型
typedef T retVal_type; // 返回值类型
public:
Rectangle(const T& m_width, const T& m_height)
: width(m_width), height(m_height), Shape<Rectangle<T>>() { }
T Area() const { return width * height; }
private:
T width;
T height;
};
template <typename T>
class Circle : public Shape<Circle<T>>
{
public:
typedef T value_type;
typedef double retVal_type;
public:
Circle(const T& m_radius) : radius(m_radius), Shape<Circle<T>>() { }
T Area() const { return Pi * radius * radius; }
private:
T radius;
};
template <typename T>
void shapeFuncTest(Shape<T>& shape)
{
shape.Area();
}
int main()
{
Circle<double> circle(10.5);
Rectangle<double> rectangle(12.3, 12.5);
shapeFuncTest(circle);
shapeFuncTest(rectangle);
return 0;
}
具体细节:见“模板与泛型编程”。
多态的应用:
visitor pattern观察者模式,一种在编译器中广泛应用的设计模式。