窥视C++细节-为什么成员初始化列表更快。
特征:类内至少有一个纯虚函数,且不能生成对象。
一般设计原则:
类是一种作用域,在该作用域内定义变量、函数。
类的作用域外,
存在继承关系时,即基类的作用域嵌套在派生类中;
b.A::val
; 调用派生类B的val成员变量b.val
;struct默认是public;class默认是private(class类的成员函数,可以访问成员变量,不用考虑是否是私有);
继承时的属性变化:
protected的作用:(子类继承时可以访问到)
基类中的某个成员函数,我们不想将其暴露,但又想在子类中能访问到,这时就可以使用protected修饰。
对象一般不用memset()清空成员变量,可以用一个构造函数来完成成员变量的初始化。
直接在类定义中实现的成员函数,会被当做inline函数来处理(能否inline成功,取决于成员函数是否简单)。
函数形参列表的默认值:
一般使用结构体描述纯粹的数据,用类描述对象;
如果类的成员也是类,创建对象时先构造成员类;销毁对象时,先析构成员类;
c++编译器可能会给自动给类添加的四个成员函数:
拷贝构造和拷贝复制函数,使用细节注意:
#include
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
#include
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类型)。
在构造对象时,调用的是拷贝构造函数;给已有对象赋值时,调用拷贝赋值运算符。
#include
#include
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成员变量初始化,必须在构造函数的初始化列表里进行,不可以通过赋值来初始化。
成员函数末尾加const修饰:
使用规范:成员函数声明和定义中,结尾都必须加上const。
const只能放在成员函数的末尾,不能放在普通非成员函数的末尾。
作用:const修饰的成员函数内部不能修改任何成员变量的值。
成员函数间的调用问题:
1)非const成员函数,既能调用const成员函数,也能调用非const成员函数;
2)const成员函数,只能调用const成员函数;
对象调用成员函数的问题:
编译器负责把存放了对象地址的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
,避免隐式类型转换。Time::Time(int tempHour, int tempMIn, int tempSec) : Hour(tempHour), MInute(tempMin), Second(tempSec)
初始化列表、赋值,本质区别:
成员是类,使用初始化列表调用的是拷贝构造函数;赋值则会先创建对象(调用默认构造函数),然后赋值。因此,初始化列表对性能略有提升。
#include
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;
}
初始化列表相比赋值,更高效。
注意:
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()
*/
自己定义的“拷贝构造函数”,会代替“系统默认的逐个成员变量的拷贝”行为自己定义的拷贝构造函数,必须要在拷贝函数中给类成员赋值。
拷贝构造一定要const修饰,为了兼容处理两种场景(传入的被拷贝对象是const和非const类型)。
如果没有自己定义拷贝构造函数,编译器会定义一个“合成拷贝构造函数”,其会将参数中的成员逐个拷贝到正在创建的对象中。
每个成员变量的类型决定了如何被拷贝:
如果成员变量是类类型,就会调用这个类的拷贝构造函数来拷贝。
源对象指向的内存,直接让临时对象指向这段内存,并打断源对象与这段内存的联系(完成所谓的内存移动)。
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
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
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
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
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;
}
注意:建议使用成员函数版本。
c++中的new做了两件事情:
operator new()
分配内存;c++中的delete做了两件事情:
operator delete()
释放内存;void* operator new(size_t size);
void operator delete(void* ptr);
#include
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函数,可以是全局函数或成员函数。
注意:重载=、[]、()、->运算符,只能通过成员函数进行重载。
类名& operator=(const 类名& 源对象)
#include
#include
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
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
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->(...)
. // 成员引用运算符
.* // 成员指针引用运算符
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
;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);
}
类名::静态成员变量; //推荐
对象.静态成员变量; //和上面等价
类名::静态成员函数(实参表); //推荐
对象.静态成员函数(实参表); //和上面等价
类名::静态变量名 = ....
;在类中,声明一个静态成员变量,并未分配内存,无法正常使用。
特点:静态成员变量属于类,不属于对象。
可以通过 类名或对象名,来引用静态成员变量。
静态成员函数类似于静态成员变量,都属于类而不是对象。
静态成员函数,仅可以调用类的静态成员变量,不可以调用普通成员变量。
静态成员函数不具有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
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;
}
}
注意:设置一个类为友元类,会破坏封装。
通过声明某个类的成员函数为本类的友元函数,它就能够访问本类中的所有成员。
友元函数不含有this指针。
友元函数可以直接调用。
友元关系不能被继承。
#include
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
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
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继承,举例:
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
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
。使用细节:
::
可以访问父类同名成员;该派生类所继承的基类中定义的成员。
基类,既能够独立存在,也能够作为派生类对象的一部分存在。
该派生类定义的自己的成员。
通过sizeof
查看,得到的是基类所有成员(包括私有成员)+派生类对象所有成员的大小。
#include
#include
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;
}
memset(void* dest, int ch, size_t count)
,可以从内存角度清空基类的私有成员。默认构造函数、拷贝构造函数、移动构造函数、赋值运算符,编译器按照合成规则自动合成(除非该派生类已经自定义过了)
创建派生类对象时,先调用直接基类的构造函数,再调用派生类的构造函数。
销毁派生类对象时,先调用派生类的析构函数,再调用基类的析构函数。
注意:
1)如果没有在派生类构造函数的初始化列表中显式调用直接基类的构造函数,则会调用直接基类的默认构造函数。
2)如果手工调用派生类的析构函数,也会自动调用基类的析构函数,即先销毁派生类后自动销毁基类。
#include
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
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子类对象时,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
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;
}
注意:
普通成员函数的地址是静态的,编译时已确定。
类对象模型中的虚函数表:
虚函数的声明与定义要求非常严格,只有在派生类中的虚成员函数与基类虚成员函数一模一样的时候(包括限定符)才会被认为是真正的虚函数。
调用虚函数执行的是**“动态绑定”**,即运行时才决定该指针对象绑定的是哪个子类(父类对象指针 = new 子类
中的子类),即最后调用哪个类的同名同参虚函数。
虚函数的实现是由两个部分组成的:虚函数表、虚函数指针。
虚函数指针:
虚函数表:
虚函数会增加访问内存开销,因类中定义了虚函数,导致编译器会给该类对象增加虚函数表指针(其中存放虚函数指针)。
每个拥有虚函数指针的类所实例化的对象,都会拥有该类中所有虚函数指针并且按照一定的顺序排列在对象的地址首部(这由编译器来保证,为了能高效的取到虚函数表),从而构成了一种表状结构,称为虚函数表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)其余也是最主要的:本类的虚函数表指针、父类的虚函数表指针(指向的是这个类中各个虚函数的入口地址),这张表解决了继承、覆盖的问题。
当子类继承了父类时,会继承其虚函数表(即父类虚函数表的指针);当子类重写父类中虚函数时,会将其继承到的虚函数表中的地址替换为重写的函数地址。
一般继承:
多重继承:
定义一个对象指针,就能够调用父类以及各个子类的同名同参函数,该对象指针必须是父类指针。
如果想通过父类指针调用子类中的同名同参函数,则父类中的函数声明前必须加virtual声明,即让该同名同参的成员函数为虚函数。
override
(虚函数专用):用于子类覆盖父类的同名同参成员函数。
虚函数声明后加override,编译器就会在父类中找同名同参的虚函数,这样如果子类中的虚函数不小心写错则编译器能及时发现并报错。
final
(虚函数专用):当父类中某个成员函数声明后加final,那么任意尝试覆盖该函数的操作都会引发报错。
// 基类不能给虚函数有意义的实现,则将其声明为纯虚函数
virtual 函数返回类型 函数名(参数列表) = 0;
在基类中声明但未定义的纯虚函数,要求任何派生类都要重定义该虚函数。否则,派生类也会变成抽象基类,不能实例化,只能创建指针和引用。
含纯虚函数的抽象基类,不能实例化对象,但可以创建指针和引用(用来**“指向派生类对象”或“引用派生类对象”**)。
#include
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
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:
为程序在运行阶段确定对象的类型,只适用于包含虚函数的类。
通过运行时类型识别检查,程序能够使用基类的指针或者引用来检查其所指向的对象的实际派生类型。
#include
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的类型))。
constructor and destructor
:
inheritance with virtual functions
:
non_virtual非虚函数:不希望derived class重新定义(override重写)它;
virtual虚函数:含有默认定义,且derived class可以重新定义(override重写)它;
pure virtual纯虚函数:没有默认定义,故derived class必须重新定义(override重写)它;
注:含有纯虚函数的类为抽象类,不能产生类对象。
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的析构函数。
委托的关系的由来:某个类要干的事,委托给另一个类。
#include
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;
};
有些情况下,必须要类的完整定义而不是类的前向声明,
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
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
#include
#include
using namespace std;
int main()
{
vector> 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
#include
#include
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
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
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
(可调用对象包装器)是一个类模板:用来装各种可调用对象,例如:function
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
#include
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
#include
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
#include
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
#include
using namespace std;
int main()
{
std::function<void(int)> f_display = [](int val) { cout << val << endl; };
f_display(42);
return 0;
}
绑定bind()
的调用结果:
#include
#include
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;
}
将成员函数与参数的bind绑定结果,存放在std::function()中。
#include
#include
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
#include
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;
}
区别:
#include
// 拷贝、赋值:拷贝赋值一个initializer_list对象,并不会拷贝列表中的元素(即拷贝、赋值出的所有对象共享表中元素)
// 底层实现:底层使用array来存储的
// **注: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
#include
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和变量的类型,之后类型占位符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使用常见的使用场景:
auto不适用的场景:
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
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
#include
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
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)...)) // 存在丢失引用的可能
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表达式”和“仿函数Functor”的实现对比:
用于关闭对象的自动类型转换,一般用于含有一个参数的构造函数。
growable containers
(会发生memory reallocation
),只有vector
和deque
。
用于限制某个类不能被继承,或者某个虚函数不能被重写。
在派生类中,把override写在成员函数的后面,表示重写了基类的虚函数,提高代码可读性。
for (auto x : vctor) {} // 序列vctor中,每个元素一次拷贝到x中
for (auto& x : vctor) {} // 相比之下,用引用省去了拷贝的动作,提高了系统的效率
for (const auto& x : vctor) {} // 如果不想循环体中,对x做任何修改,可加上const关键字修饰
在c++自定义的类中,编译器会默认生成一些成员函数:无参构造/拷贝构造/拷贝赋值/移动构造/移动赋值/析构函数。
class Girl
{
private:
int age;
string name;
public:
Girl() = default; // 使用编译器默认的构造函数
Girl(const Girl& girl) = delete; // 禁用该构造函数
~Girl() = default;
};
注意:如果类的成员变量中不含有指针成员变量,则使用默认的构造函数基本都可以。
typedef void(*funcPtr)(int);
using funPtr = void(*)(int);
using Vec= vector<int, allocator<int>>;
typedef vector<int, allocator<T>> Vec;
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
#include
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
关键字,表示空指针,其是指针类型不是整型类型;nullptr==0
该表达式是true;多用于多线程中。
// 传统的枚举类
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类型转为数值
左值:可出现在operator=的左/右侧。
右值:只能出现在operator=的右侧;可调用移动构造/移动赋值函数,可进行资源的转移。
左值引用和右值引用的区别:
c++中的垃圾回收GC机制
左值引用、右值引用的差异:
1、常量(左值)引用,可指向右值
2、std::move()
,可将右值引用指向左值
3、声明出来的左值、右值引用,都是左值
4、功能差异:
左值引用,可避免对象的拷贝(一般在传参时加常量左值引用,避免无用的对象拷贝)
右值引用:
1)移动语义(在移动构造函数和移动赋值函数中)
2)完美转发:函数模板可以将参数完美的转发给其内部调用的函数,即将值以及值的左右属性“透传”到内部调用的函数中。
实现机制:万能引用&&
,利用了“引用折叠”规则;std::forward()
,还原值类型。
#include
#include
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转换
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
#include
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
#include
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_)
*/
inherting constructor
,即在派生类中使用using来声明继承基类的构造函数;对参数进行了泛化,能支持任意个数、任意数据类型的参数。
如果传入的可变参数类型不同,则详见“泛型编程”。
如果传入的可变参数类型相同,则使用initializer_list
足以。
#include
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;
}
主流编程语言中,c++是唯一依赖RAII(resource acquisition is initialization)来做资源管理的。
RAII依托栈 和 构造/析构函数,来对所有资源(包括堆内存在内的内存进行管理),
RAII有一些比较成熟的智能指针,借助“引用计数”来完成垃圾回收,比如std::auto_ptr(c++11已经废弃)、std::shared_ptr、std::weak_ptr、std::unique_ptr。
T*
的变量,能保存一个类型T
的对象的地址。*
,可以间接的访问指针变量所指向的内存地址中的值,该过程称为间接访问/引用指针。指针的算数运算:p + i == p + i*d
(i为整数,d为p指向的变量占字节数),p指向的内存区域,存储相同的数据类型。
p1 - p2
= 两指针间元素个数 = (p1 - p2) / d
。p1 + p2
无意义。指针变量的赋值运算:
一维数组:
p = array
/ p = &array[i]
/ p=array,*(p++)
:将数组元素地址存放在地址变量p所在的地址。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,表示p1
指的元素在前;
p1>p2
,表示p1
指的元素在后;p1==p2
,表示p1
与p2
指向同一元素;变量、地址和指针变量:
一般类型指针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出来的动态内存
}
三级指针:***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++中都是用于间接访问其他对象或值的工具。然而,它们有一些关键的区别,这些区别使得在某些情况下使用引用比使用指针更好,反之亦然。
因此,虽然指针和引用在功能上有一些重叠,但它们各自有独特的优点和用途,这就是为什么我们既需要指针又需要引用。
直接内存管理(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[] 应该用 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[]释放(内部逻辑较为复杂,不考虑)
裸指针(直接用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出来的对象)的生命周期管理,防止内存泄漏”。
智能指针的类型:
独占式(专属所有权):同一时刻,只能有一个unique_ptr指针指向这个内存对象。
#include
中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
#include
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;
}
#include
#include
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 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 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指定的删除器,更加灵活,且不作为类型判断标准。
通常情况下,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* 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(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<指向的类型> 智能指针名
,多个指针(使用引用计数机制,来表明资源被几个指针共享)指向同一个对象(共享所有权),直到最后一个指向它的指针被销毁时,这个对象才会被销毁。
智能指针是explicit(显式类型转换),不能进行隐式类型转换(一般用等号初始化),故要进行直接初始化(即用()圆括号初始化)。
shared_ptr<int> makes(int value)
{
// return 临时对象
return shared_ptr<int>(new int(value));
// 报错,无法从 new 得到的 int* 转换到 shared_ptr
//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
#include
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的拷贝都指向相同的内存;
引用计数(又称强引用计数strong refs
),会记录指向该对象内存的指针个数;只有当最后一个指向该内存(对象)的shared_ptr指针不再需要指向该对象时,即引用计数为0时shared_ptr指针才会析构所指向的内存对象。
注意:弱引用计数,一般会和weak_ptr
搭配使用,解决交叉引用的问题。
weak refs
,不会改变所指向内存对象的生命周期,只有强引用才可以改变生命周期。自定义的删除器:在创建一个数组并用智能指针指向它(需要通过删除器,调用delete[]来析构,因默认采用delete直接析构) / 在线程池队列中拿到一个线程并封装为一个智能指针(归还线程时,会重新添加到队列中,而不是直接析构)时,需要自定义或使用默认删除器default_delete
。
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,故会自动析构掉
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;
}
通过 指定删除器
取代系统提供的默认删除器(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 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对象同时写,则需要加锁;
#include
#include
#include
#include
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;
}
// 这样,多个线程可以同时读取共享指针的值,但只有一个线程能够修改它。
#include
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(不会修改引用计数的值,只会增加弱引用计数),故会打破这种循环引用,且可以检测到它所管理的对象是否已经被释放。
为了避免出现循环引用的问题,可以使用weak_ptr和shared_ptr共同作用,即用一种观察者模式。
class B;
class A
{
public:
~A()
{
cout<<"A delete\n";
}
public:
// weak_ptr pb_; // 采用weak_ptr,解决循环引用导致引用计数无法降为0的问题
shared_ptr<B> pb_;
};
class B
{
public:
~B()
{
cout<<"B delete\n";
}
public:
// weak_ptr 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;
}
和shared_ptr尺寸是相同的,是裸指针的2倍。
内存对象、控制块(强引用计数strong refs、弱引用计数weak_refs、其他数据(自定义删除器、分配器))是由shared_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
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
#include
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
#include
#include
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指定的删除器,更加灵活,且不作为类型判断标准。
#include
#include
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和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
#include
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;
}
is_shareable
,为false则表示该内存块不可再被共享,之后再调用拷贝构造则需要重新申请内存空间。写时复制copy-on-write、外部加锁、内部加锁。
外部加锁:由调用者负责加锁,决定“跨线程”使用“共享内存”时,加锁的时机。
内部加锁(不常见):对象将所有对自己的访问串行化(通过为每个成员函数加锁实现),故不再需要在多线程中共享该对象时进行外部加锁。
copy-on-write
的使用场景/代价:
使用场景:当字符串重复次数越多,内存节省的效果越明显,否则copy-on-write的设计则没有必要。
代价:引入了EPString
这中内部的辅助类,故会额外的占用内存空间;在String类实现过程中,额外的增加的代码,也提高了程序的复杂度。
(不考虑多态时)父类指针指向子类对象、子类指针指向父类对象:
虚函数的,就是为了避免指向子类对象的父类指针,只能调用父类的成员函数,而设计的。
静态多态:编译时就已确定要执行函数的入口地址,主要有:函数重载、函数模板、类模板。
动态多态:通过虚函数机制实现,即在运行期间动态绑定(若子类没有重写父类中的虚函数,则多态是一件毫无意义的事情)。
#include
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;
}
通过继承、虚函数(“父类指针/父类引用”绑定子类对象),可以实现运行时多态:
访问权限:
虚函数:
虚函数表,可认为是子类隐藏的一个成员数组(数组中标注每个虚函数的入口地址)。
编译器和运行时环境,通过虚函数表,保证调用正确版本的函数。
父类成员函数前加virtual,即允许子类重写基类的虚函数,并通过重写实现运行时多态。
若基类定义了纯虚函数,则要求子类必须重写该函数。
final修饰父类的虚函数,则要求子类不能重写该虚函数。
override修饰子类的虚函数,则表明该虚函数是重写父类的。
运行时多态:
#include
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
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
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
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观察者模式,一种在编译器中广泛应用的设计模式。