改善程序与设计的55个具体做法
C++是一个多重范型编程语言,支持过程形式、面向对象形式、函数形式、泛型形式、元编程形式,C++高效编程守则视状况而变化,取决于使用C++哪一个部分,在合适的场景选择使用合适的功能
C开发中以前都在使用宏定义define,但是往往难以维护而且难以调试
#define ASPECT_RATIO 1.653
//使用const替换
const double AspectRatio = 1.653;
const的底层const与顶层const要知道
const char* const authorName = "Scott Meyers";//字符串用const代替宏
//cpp等推荐使用string,更加抽象
const std::string authorName("Scott Meyers");
对于类内的可以使用静态成员变量
class A{
private:
static const int Num = 5;//常量声明式
int scores[Num];
};
如果不使用A::Num的地址,那么只使用声明式即可,如果要用地址则需要定义式在源文件中
const int A::Num;//Num定义,声明时已经提供初值,定义式不再需要提供初值
//如果编译器不支持声明式初始化,则要在定义式提供初值
类内的enum更像define,不能取值
class A
{
public:
enum
{
= 4
Num };
int arr[Num];
};
int main(int argc, char **argv)
{
<< A::Num << endl; // 4
cout return 0;
}
关于define写函数形式的一些问题,尽量使用inline,一般inline函数写在头文件中,因为源文件编译时需要将函数展开
#define MAX(a, b) (a) > (b) ? (a) : (b)
int main(int argc, char **argv)
{
int n = MAX(1, 2);
<< n << endl; // 2
cout // int n1 = MAX(n++, ++n); 这种问题容易混乱
return 0;
}
//尽可能使用inline函数,也可以使用模板进行扩展
//函数也能展开,而且利于开发维护
template <typename T>
inline T mymax(const T &a, const T &b)
{
return a > b ? a : b;
}
const有顶层const(一般为指针不能修改)与底层const(数据不能修改),
char greeting[] = "hello";
char *p = greeting;//none-const pointer,non-const data
const char* p =greeting;//non-const pointer,const data
char* const p = greeting;//const pointer,non-const data
const char* const p = greeting;//const pointer,const data
有两种形式的表达的意思是相同的,都是data const
void func(char const *p);
void func(const char*p);
函数返回常量值的作用
class A
{
public:
(const int &n) : num(n)
A{
}
int num;
const A operator*(const A &o)
{
return A(this->num * o.num);
}
};
int main(int argc, char **argv)
{
(1);
A a(2);
A b= a * b;
A c //(a * b) = 3; // 错误:操作数类型为: const A = int,如果返回的不是const值则不报错
<< c.num << endl; // 2
cout return 0;
}
const成员函数
class A
{
public:
static const int num{9};
char arr[num] = {0};
const char &operator[](std::size_t position) const
{
return arr[position];
}
char &operator[](std::size_t position)
{
return arr[position];
}
};
int main(int argc, char **argv)
{
;
A aconst A b;
<< a[0] << endl; // 调用char &A::operator[]
cout << b[0] << endl; // 调用 const char &A::operator[]
cout // b[0] = '1'; 错误
[0] = 'a';
a<< a[0] << endl; // a
cout return 0;
}
在const和non-const成员函数中避免重复,可以让non-const调用const成员函数
class A
{
public:
static const int num{9};
char arr[num] = {0};
const char &operator[](std::size_t position) const
{
//...
// ...
return arr[position];
}
char &operator[](std::size_t position)
{
return const_cast<char &>(static_cast<const A &>(*this)[position]);
}
};
int n;
<< n << endl; cout
会输出什么,大部分都会说0,但是不一定,有随机性,不能相信机器与编译器,加上个初始值不会杀了你
为什么要使用初始化列表,而不是在构造函数内赋值
class A
{
public:
()
A{
<< "A()" << endl;
cout }
(const int &n)
A{
<< "A(const int &n)" << endl;
cout }
const A &operator=(const int &n)
{
<< "const A &operator=(const int &n)" << endl;
cout return *this;
}
};
class B
{
public:
()
B{
= 1; // 这是赋值不是初始化
a }
;
A a};
int main(int argc, char **argv)
{
;
B b// A()
// const A &operator=(const int &n)
return 0;
}
如果使用构造函数列表,It’s fucking cool.特别注意的是初始化列表为什么要与在类内声明的顺序相同,这是因为它们构造的现后顺序并不取决于在初始化列表中的顺序而是在类内声明的顺序所以我们写代码直接把二者顺序同步好了。
class B
{
public:
() : a(1)
B{
}
;
A a};
int main(int argc, char **argv)
{
;
B b// A(const int &n)
return 0;
}
什么是local-static对象和non-local static对象,栈内存与堆内存对象都不是static对象。像全局对象、定义在命名空间作用域内的、在class内的、在函数内的、以及在源文件作用域内的被声明为static的对象。其中在函数内的为local-static其他为non-local static。程序结束时static会被自动销毁,析构函数在main返回前调用
可能有时会使用extern访问在其他源文件定义的对象,如果一个源文件中某个non-local static对象初始化时用到了另一个源文件中的non-local static对象,可能会出现赋值操作右边的变量没有初始化过的情况,因为C++中:对于“定义于不同源文件内的non-local static对象”的初始化次序并无明确定义
//mian.cpp
extern int n;
int n1=n;
//main1.cpp
int n;
怎样解决这一问题,推荐使用local static代替non-local static
//main.cpp
int n1=n();
//main1.cpp
int& n(){
static int v=100;
return v;
}
上面例子可能还不清楚看下面这个
//main.cpp
#include <iostream>
#include "main1.h"
#include "main2.h"
using namespace std;
int main(int argc, char **argv)
{
return 0;
}
//main1.h
#pragma once
class main1
{
public:
();
main1};
//main1.cpp
#include "main1.h"
#include <iostream>
;
main1 main1Object
::main1()
main1{
std::cout << "main1" << std::endl;
}
//main2.h
#pragma once
#include "main1.h"
class main2
{
private:
/* data */
public:
(/* args */);
main2};
//main2.cpp
#include "main2.h"
#include <iostream>
;
main2 main2Object
::main2(/* args */)
main2{
std::cout << "main2" << std::endl;
}
请问main1和main2谁先输出,答案是不确定的,所以总之记住全局变量之间不要互相引用初始化,特别是在不同源文件中的不同全局变量。
@DESKTOP-QDLGRDB:/mnt/c/Users/gaowanlu/Desktop/MyProject/note/testcode$ g++ -c main1.cpp
gaowanlu@DESKTOP-QDLGRDB:/mnt/c/Users/gaowanlu/Desktop/MyProject/note/testcode$ g++ -c main2.cpp
gaowanlu@DESKTOP-QDLGRDB:/mnt/c/Users/gaowanlu/Desktop/MyProject/note/testcode$ g++ -c main.cpp
gaowanlu@DESKTOP-QDLGRDB:/mnt/c/Users/gaowanlu/Desktop/MyProject/note/testcode$ g++ main.o main1.o main2.o -o main.exe
gaowanlu@DESKTOP-QDLGRDB:/mnt/c/Users/gaowanlu/Desktop/MyProject/note/testcode$ ./main.exe
gaowanlu
main1
main2@DESKTOP-QDLGRDB:/mnt/c/Users/gaowanlu/Desktop/MyProject/note/testcode$ g++ main.o main2.o main1.o -o main.exe
gaowanlu@DESKTOP-QDLGRDB:/mnt/c/Users/gaowanlu/Desktop/MyProject/note/testcode$ ./main.exe
gaowanlu
main2
main1@DESKTOP-QDLGRDB:/mnt/c/Users/gaowanlu/Desktop/MyProject/note/testcode$ gaowanlu
还心存执念,那你循环引用下,初始化肯定有问题吧,n1在main.cpp,n2在main1.cpp,n1用n2初始化,n2用n1初始化。这样虽然能编译,能运行,但它们的初始化确实有问题。
默认生成这些函数是C++的基础知识,应该问题不大,当程序中使用这些函数时编译器才会生成,如果自己声明了自定义的相关函数则编译器不再自动生成默认的对应函数
class A
{
public:
() {}
A~A() {}
(const A &a)
A{
this->num = a.num;
}
&operator=(const A &a)
A {
this->num = a.num;
return *this;
}
int num;
};
//写为private,只声明不定义
class A
{
public:
() {}
A~A() {}
private:
(const A &a); // 只声明不定义
A&operator=(const A &a);
A };
//使用delete关键词
class B
{
public:
() {}
B~B() {}
(const B &b) = delete;
B&operator=(const B &b) = delete;
B };
int main(int argc, char **argv)
{
;
A a;
A b// a = b; 错误
return 0;
}
还可以使用Uncopyable基类的方式,在基类进行拷贝构造和赋值时,会先执行基类的相关函数
class A
{
public:
() {}
A(const A &a)
A{
<< "A(const A&a)" << endl;
cout }
&operator=(const A &a)
A {
<< "A& operator=(const A&a)" << endl;
cout return *this;
}
virtual ~A() = default;
};
class B : public A
{
public:
() {}
B(const B &b) : A(b)
B{
<< "B(const B&b)" << endl;
cout }
&operator=(const B &b)
B {
if (&b != this)
{
::operator=(b);
A}
<< "B &operator=(const B &b)" << endl;
cout return *this;
}
~B() = default;
};
int main(int argc, char **argv)
{
;
B b1= b1;
B b2 // A(const A&a)
// B(const B &b)
return 0;
}
那么就可以写一个Uncopyable基类
class A
{
public:
() {}
Avirtual ~A() = default;
private:
(const A &a);
A&operator=(const A &a);
A };
class B : public A
{
public:
() {}
B~B() = default;
// 理应当自动生成拷贝构造和赋值操作函数,但是由于不能访问基类部分,所以不能自动生成
};
int main(int argc, char **argv)
{
;
B b1// B b2 = b1;
// 无法引用 函数 "B::B(const B &)" (已隐式声明) -- 它是已删除的函数
return 0;
}
先看以下有什么搞人的事情,深入理解此部分要对虚函数表以及C++多态机制有一定了解,下面的代码只执行了基类的析构函数只是释放了基类中buffer的动态内存,而派生类部分内存泄露,这是因为A*a
,a被程序认为其对象只是一个A,而不是B,如果将基类析构函数改为virtual的,那么会向下找,找到~B执行,然后再向上执行如果虚函数有定义的话
class A
{
public:
() : buffer(new char[10])
A{
}
~A()
{
<< "~A()" << endl;
cout delete buffer;
}
private:
char *buffer;
};
class B : public A
{
public:
() : buffer(new char[10])
B{
}
~B()
{
<< "~B()" << endl;
cout delete buffer;
}
private:
char *buffer;
};
int main(int argc, char **argv)
{
*a = new B;
A delete a;
//~A()
return 0;
}
所以要修改为这样,即可
class A
{
public:
() : buffer(new char[10])
A{
}
virtual ~A()
{
<< "~A()" << endl;
cout delete buffer;
}
private:
char *buffer;
};
如果想让基类为抽象类,可以改为纯虚函数,与前面不同的时拥有纯虚函数的类为抽象类不允许实例化,纯虚函数不用定义。而虚函数是需要有定义的。
class A
{
public:
() {}
Avirtual ~A() = 0;
};
::~A() {}
A
class B : public A
{
};
int main(int argc, char **argv)
{
// A a; 错误A为抽象类型
;
B breturn 0;
}
例如以下情况
void freeA()
{
throw runtime_error("freeA() error");
}
class A
{
public:
() {}
A~A()
{
try
{
();
freeA}
catch (...)
{
// std::abort();//生成coredump结束
// 或者处理异常
//...
}
}
};
int main(int argc, char **argv)
{
*a = new A;
A delete a;
return 0;
}
如果外部需要对某些在析构函数内的产生的异常进行操作等,应该提供新的方法,缩减析构函数内容
void freeA()
{
throw runtime_error("freeA() error");
}
class A
{
public:
() {}
A~A()
{
if (!freeAed)
{
try
{
();
freeA}
catch (...)
{
// std::abort();//生成coredump结束
// 或者处理异常
//...
}
}
}
void freeA()
{
::freeA();
= true;
freeAed }
private:
bool freeAed = {false};
};
int main(int argc, char **argv)
{
*a = new A;
A try
{
->freeA();
a}
catch (const runtime_error &e)
{
<< e.what() << endl;
cout }
delete a;
return 0;
}
1、构造函数中调用虚函数:
当在基类的构造函数中调用虚函数时,由于派生类的构造函数尚未执行,派生类对象的派生部分还没有被初始化。这意味着在基类构造函数中调用的虚函数将无法正确地访问或使用派生类的成员。此外,派生类中覆盖的虚函数也不会被调用,因为派生类的构造函数尚未执行完毕。
2、析构函数中调用虚函数:
当在基类的析构函数中调用虚函数时,如果正在销毁的对象是一个派生类对象,那么派生类的部分已经被销毁,只剩下基类的部分。此时调用虚函数可能会导致访问已被销毁的派生类成员,从而引发未定义行为。
以下程序是没问题的
class A
{
public:
()
A{
();
func}
virtual ~A()
{
();
func}
virtual void func()
{
<< "A::func" << endl;
cout };
};
class B : public A
{
public:
()
B{
();
func}
~B()
{
();
func}
void func() override
{
<< "B::func" << endl;
cout }
};
int main(int argc, char **argv)
{
;
B b// A::func 此时只有A::func 无B::func
// B::func 此时在执行B构造函数故执行B::func
// B::func 此时在执行B析构函数故执行B::func
// A::func 此时在执行A析构函数只有A::func 无B::func
return 0;
}
像+=、-=、*=操作符函数可以没有返回值,但是如果想有赋值连锁形式就要返回引用
class A
{
public:
()
A{
}
virtual ~A()
{
}
void operator=(const A &a)
{
<< "=" << endl;
cout }
};
int main(int argc, char **argv)
{
;
A a1;
A a2= a2; //=
a1 return 0;
}
赋值连锁形式,如果想要支持这种形式就要返回引用
int x1, x2, x3;
= x2 = x3 = 1;
x1 << " " << x1 << " " << x2 << " " << x3 << endl; // 1 1 1
cout //自定义为
&operator=(const A &a)
A {
<< "=" << endl;
cout return *this;
}
;
Object obj=obj;//这不是有病吗 obj
如何判断与解决此问题呢,或者定义使用std::swap(需要定义swap方法或重写operator=)
class A
{
public:
virtual ~A()
{
}
&operator=(const A &a)
A {
if (this == &a)
{
<< "self" << endl;
cout return *this;
}
<< "other" << endl;
cout //----------------------------------------------------
(a); // 临时副本,一面在复制期间a修改了导致数据不一致
A temp// 赋值操作
//...
//----------------------------------------------------
return *this;
}
};
int main(int argc, char **argv)
{
;
A a= a; // self
a ;
A a1= a1; // other
a return 0;
}
可能一开始的业务是这样,但后来加上了isman属性,但是你却忘了加到拷贝构造和赋值函数中,那么这是异常灾难,可能你还找不出来自己错在哪里
class A
{
public:
() {}
A(const A &a) : num(a.num)
A{
}
&operator=(const A &a)
A {
this->num = a.num;
}
int num;
//bool isman;
};
还有更恐怖的风险,在存在继承时,你可能忘记了基类部分,所以千万不能忘记
class A
{
public:
() {}
Avirtual ~A(){};
(const A &a) : num(a.num)
A{
}
&operator=(const A &a)
A {
this->num = a.num;
return *this;
}
int num;
};
class B : public A
{
public:
() : A()
B{
}
~B() {}
(const B &b) : A(b), priority(b.priority) // 不要忘记
B{
}
&operator=(const B &b)
B {
::operator=(b); // 不要忘记
Athis->priority = b.priority;
return *this;
}
int priority;
};
下面的就是风险较大的情况
void func()
{
int *ptr = new int(5);
// ...
// ... 做许多事情,中间可能会return,措施delete执行
delete ptr;
}
请记住:
#include <iostream>
using namespace std;
class RAII
{
public:
(int *ptr)
RAII{
m_ptr = ptr;
}
~RAII()
{
if (m_ptr)
{
delete m_ptr;
}
}
int *get()
{
return m_ptr;
}
operator int()
{
if (m_ptr)
{
return *m_ptr;
}
return 0;
}
operator int *()
{
return m_ptr;
}
private:
int *m_ptr{nullptr};
};
int main(int argc, char **argv)
{
(new int(9));
RAII ptr*ptr.get() = 323;
std::cout << (*ptr.get()) << std::endl; // 323
int *resource = ptr;
std::cout << *resource << std::endl; // 323
return 0;
}
[]
,必须在相应得delete表达式中也使用[]
。如果你在new表达式中不使用[]
,一定不要在相应delete表达式中使用[]
#include <iostream>
using namespace std;
int main(int argc, char **argv)
{
int *arr = new int[100];
int *ptr = new int;
delete[] arr;
delete ptr;
return 0;
}
这句话什么意思呢?
void function(RAII raii, int i)
{
// do something
}
(RAII(new int(1)), otherFunction(199)); function
这样可能存在内存泄露的风险,因为可能出现下面的情况
万一中间调用otherFunction出现异常就完蛋了,所以请遵守规则,独立创建RAII
(new int(1));
RAII raii(raii, otherFunction(199)); function
设计一个组件,那么设计外部接口是非常重要的,正常使用的情况下,还应该做到不易用错,例如
class Date
{
public:
(int month, int day, int year);
Date};
外部调用Date很容易将三个参数写错,或者写反,造成目的与实际效果不同,很难排查。上面的例子,可以为每类参数设计一个类,如
struct Day
{
explicit Day(int d) : val(d)
{
}
int val;
}
struct Month()
{
static Month Jan() { return Month(1); }
...
};
struct Year()...
class Date
{
public:
(const Month& month, const Day& day, const Year& year);
Date};
例如工厂函数
* createInvestment();
Investmentvoid getRidOfInvestment(Investment);
这样很容易内存泄露,所以要使用智能指针
std::shared_ptr<Investment> createInvestment();
关于使用智能指针则可以为智能指针指定销毁函数,解决跨DLL问题(例如再某个申请的内存指针地址传到另一个DLL使用了另一个DLL的delete,我们尽可能遵循那个DLL申请的内存则由那个DLL的销毁函数进行释放)
std::shared_ptr<Investment> createInvestment()
{
*ptr = new Investment;
Investment std::shared_ptr<Investment> ret(ptr, [](Investment *ptr) -> void
{ delete ptr; });
return ret;
}
请记住
设计新的class应该带着“语言设计者当初设计语言内置类型时”一样的严谨来讨论class的设计。
新type的对象应该如何被创建和销毁?
构造函数和析构函数以及内存分配函数和释放函数operator new
、operator new[]
、operator delete
、operator delete[]
。
对象的初始化和对象的赋值该有什么样的差别?
决定构造函数和赋值操作符的行为。
对象如果被以值传递,意味着什么?
什么是新type的和法值? 构造函数,赋值操作符,setter等。
新类型需要配合某个继承图系吗?
受到函数是virtual或non-virtual的影响,如果允许其他class继承,则会影响所声明的函数尤其是析构函数,是否为virtual。
新类型需要什么样的转换?
写转换函数operator TYPE
或者写non explicit one
argument构造函数等。
什么样的操作符和函数对新类型是合理的?
应该声明哪些函数,操作符成员函数del 自定义功能等等。
什么样的标准函数应该驳回? 声明为private。
谁该取用新类型的成员?
决定合理设计成员的public、protected、private。
什么是新类型读的未声明接口?
新class有多么一般化?
合理使用模板编程。
你真的需要一个新class吗?
根据功能实际情况合理设计。
#include <iostream>
using namespace std;
class A
{
public:
// big content...
};
void dosomething(const A &a)
{
}
int main(int argc, char **argv)
{
;
A a(a);
dosomethingreturn 0;
}
切割问题
#include <iostream>
using namespace std;
class A
{
public:
virtual void run()
{
std::cout << "A::run" << std::endl;
}
};
class B : public A
{
public:
void run() override
{
std::cout << "A::run" << std::endl;
}
};
int main(int argc, char **argv)
{
;
A a// B *b_ptr = dynamic_cast<B *>(&a); // 会报错
*b_ptr = (B *)&a; // 不会报错
B ->run(); // 产生切割问题 输出A::run
b_ptr;
B b_instance= b_instance; // 产生切割问题 只拷贝了基类部分
A a_copy_from_b .run(); // A::run
a_copy_from_breturn 0;
}
如下面的场景返回值类型就挺好的
#include <iostream>
using namespace std;
class Rational;
const Rational operator*(const Rational &lhs, const Rational &rhs);
class Rational
{
public:
(int numberator = 0, int denominator = 1);
Rational
private:
int n, d;
friend const Rational operator*(const Rational &lhs, const Rational &rhs);
};
::Rational(int numberator, int denominator) : n(numberator), d(denominator)
Rational{
}
// 比较好的方式
const Rational operator*(const Rational &lhs, const Rational &rhs)
{
return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}
int main(int argc, char **argv)
{
return 0;
}
下面返回了local stack 的引用,则会core
#include <iostream>
using namespace std;
class Rational;
const Rational &operator*(const Rational &lhs, const Rational &rhs);
class Rational
{
public:
(int numberator = 0, int denominator = 1);
Rational
private:
int n, d;
friend const Rational &operator*(const Rational &lhs, const Rational &rhs);
};
::Rational(int numberator, int denominator) : n(numberator), d(denominator)
Rational{
}
const Rational &operator*(const Rational &lhs, const Rational &rhs)
{
(lhs.n * rhs.n, lhs.d * rhs.d);
Rational retreturn ret;
}
int main(int argc, char **argv)
{
, r2;
Rational r1= r1 * r2;
Rational r3 return 0;
}
返回local static的引用,也会存在一定问题
#include <iostream>
using namespace std;
class Rational;
const Rational &operator*(const Rational &lhs, const Rational &rhs);
class Rational
{
public:
(int numberator = 0, int denominator = 1);
Rationalbool operator==(const Rational &other) const
{
return this->n == other.n && this->d == other.d;
}
private:
int n,
;
dfriend const Rational &operator*(const Rational &lhs, const Rational &rhs);
};
::Rational(int numberator, int denominator) : n(numberator), d(denominator)
Rational{
}
// 比较好的方式
const Rational &operator*(const Rational &lhs, const Rational &rhs)
{
static Rational ret;
.n = lhs.n * rhs.n;
ret.d = lhs.d * rhs.d;
retreturn ret;
}
int main(int argc, char **argv)
{
(1, 2), r2(3, 4);
Rational r1(5, 6), r4(7, 9);
Rational r3std::cout << boolalpha << ((r1 * r2) == (r3 * r4)) << std::endl; // 总是返回true因为比较的是同一个Rational对象当然总是相等
return 0;
}
假设我们有一个public成员变量,而我们最终取消了它。多少代码可能会被破坏呢?所有使用它的客户代码都会被破坏,而那是一个不可知的大量。因此public成员变量完全没有封装性。
假设我们有一个protected成员变量,而我们最终取消了它,有多少代码被破坏? 所有使用它的derived classes都会被破坏,那往往也是个不可知的大量。
因此,protected成员变量就像public成员变量一样缺乏封装性,
因为在这两种情况下,如果成员变量被改变,都会有不可预知的大量代码受到破坏。 一旦你将一个成员变量声明为public或protected而客户开始使用它,就很难改变那个成员变量所涉及的一切。太多代码需要重写、重新测试、重新编写文档、重新编译。
从封装的角度观之,其实只有两种访问权限:private(提供封装)和其他(不提供封装)。
class WebBrowser
{
public:
// ...
void clearCache();
void clearHistory();
void removeCookies();
void clearEveryhing()
{
();
clearCache();
clearHistory();
removeCookies}
// ...
};
不如写为
namespace WebBrowserStuff
{
class WebBrowser
{
public:
// ...
void clearCache();
void clearHistory();
void removeCookies();
// ...
};
void clearBrowser(WebBrowser &wb)
{
.clearCache();
wb.clearHistory();
wb.removeCookies();
wb}
}
还可以根据功能划分与重要成都写到不同的头文件中
// webbrowser.h
namespace WebBrowserStuff
{
class WebBrowser{...};
// ... 核心机能,如几乎所有客户端需要的non-member函数
}
// webbrowserbookmarks.h
namespace WebBrowserStuff
{
// ... 与书签相关的便利函数
}
// 头文件 webbrowsercookies.h
namspace WebBrowserStuff{
// ... 与cookie相关的便利函数
}
只看描述是很难理解的,看代码的例子,就好很多。
class Rational
{
public:
(int numerator = 0,
Rationalint denominator = 1); // 构造函数刻意不位explicit 允许int-to-Rational隐式转换
int numerator() const;
int denominator() const;
private:
...
};
自定义乘法运算符
class Rational
{
public:
...
const Rational operator* (const Rational& rhs) const;
};
(1, 8);
Rational oneEighth(1, 2);
Rational oneHalf= oneHalf * oneEighth;
Rational result = result * oneEighth;
result = oneHalf.operator*(2);
result = 2.operator*(oneHalf); // 错误
result = operator*(2, oneHalf); // 错误 result
上面的oneHalf.operator*(2)
相当于做了隐式转换
const Rational temp(2);
= oneHalf * temp; // Rational 构造函数是非explicit的 result
想要支持混合式算术运算,让operator*
成为一个non-member函数,允许编译器在每一个实参上执行隐式类型转换:
class Rational
{
... // 不包括operator*
};
// 定义为non-member函数
const Rational operator*(const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.numberator() * rhs.numberator(), lhs.denominator() * rhs.denominator());
}
(1, 4);
Rational oneFourth;
Rational result= oneFourth * 2;
result = 2 * oneFourth; result
std::swap
对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。std::swap
。std::swap
使用using生命,然后调用swap并且不带任何“命明空间资格修饰”std
内加入某些对std而言全新的东西。所谓swap(置换)两个对象值,意思是将两对象的值彼此赋予对方。缺省情况下swap动作可由标准程序库提供的swap算法完成。
namespace std
{
template <typename T>
void swap(T &a, T &b)
{
(a);
T temp= b;
a = temp;
b }
}
只要类型T支持copying(通过拷贝构造函数和拷贝赋值操作符完成)。
#include <iostream>
#include <vector>
using namespace std;
class AImpl
{
public:
int a, b, c;
std::vector<int> vec;
};
class A
{
public:
()
A{
= new AImpl;
implPtr }
~A()
{
if (implPtr)
{
delete implPtr;
}
}
(const A &a)
A{
= new AImpl(*a.implPtr);
implPtr }
&operator=(const A &a)
A {
// 深拷贝 不然默认只拷贝地址有问题
*implPtr = *a.implPtr;
return *this;
}
*implPtr;
AImpl };
int main(int argc, char **argv)
{
;
A a1= a1;
A a2 return 0;
}
如上面的例子,因为需要拷贝我们必须写深拷贝,不然默认进行地址拷贝会出问题。但是使用std::swap
时就显得有些鸡肋,明明只交换二者的
implPtr存储的地址即可,却使用的拷贝。但是我们对std::swap
针对A进行特化。
namespace std
{
template <>
void swap<A>(A &a, A &b)
{
(a.implPtr, b.implPtr);
swap}
}
上面可以实现,是因为implPtr属性是public的,如果为私有的时应该怎么做
#include <iostream>
#include <vector>
using namespace std;
class A;
class AImpl;
void swap(A &a, A &b) noexcept;
class AImpl
{
public:
int a, b, c;
std::vector<int> vec;
};
class A
{
public:
()
A{
= new AImpl;
implPtr }
~A()
{
if (implPtr)
{
delete implPtr;
}
}
(const A &a)
A{
= new AImpl(*a.implPtr);
implPtr }
&operator=(const A &a)
A {
// 深拷贝 不然默认只拷贝地址有问题
*implPtr = *a.implPtr;
return *this;
}
public:
friend void swap(A &a, A &b) noexcept;
void swap(A &b) noexcept
{
::swap(*this, b);
}
private:
*implPtr;
AImpl };
void swap(A &a, A &b) noexcept
{
std::cout << "my swap" << std::endl;
std::swap(a.implPtr, b.implPtr);
}
namespace std
{
template <>
void swap<A>(A &a, A &b) noexcept
{
::swap(a, b);
}
}
int main(int argc, char **argv)
{
;
A a1= a1;
A a2 std::swap(a1, a2); // my swap
(a1, a2); // my swap
swapreturn 0;
}
如果A是一个模板类是,情况则有些麻烦。在C++中,模板的特例化不能放在std命名空间中,除非标准库特意允许。因为直接在std命名空间中特例化会导致不确定行为。
#include <iostream>
#include <vector>
using namespace std;
namespace ASpace
{
template <typename T>
class A;
class AImpl;
template <typename T>
void swap(A<T> &a, A<T> &b) noexcept;
class AImpl
{
public:
int a, b, c;
std::vector<int> vec;
};
template <typename T>
class A
{
public:
()
A{
= new AImpl;
implPtr }
~A()
{
delete implPtr;
}
(const A &a)
A{
= new AImpl(*a.implPtr);
implPtr }
&operator=(const A &a)
A {
if (this == &a)
{
return *this; // nothing todo
}
// 深拷贝 不然默认只拷贝地址有问题
*implPtr = *a.implPtr;
return *this;
}
public:
friend void swap<>(A<T> &a, A<T> &b) noexcept;
void swap(A &b) noexcept
{
std::swap(implPtr, b.implPtr);
}
private:
*implPtr;
AImpl };
template <typename T>
void swap(A<T> &a, A<T> &b) noexcept
{
std::cout << "my swap" << std::endl;
.swap(b);
a}
} // ASpace
int main(int argc, char **argv)
{
::A<ASpace::AImpl> a1;
ASpace::A<ASpace::AImpl> a2 = a1;
ASpace(a1, a2); // my swap // 触发(argument-dependentlookup或Koenig lookup法则)
swap// std::swap(a1,a2);//error
{
using std::swap;
(a1, a2); // ADL(Argument Dependent Lookup,参数依赖查找) 调用ASpace::swap
swap}
return 0;
}
只要定义了一个变量其类型带有一个构造函数或析构函数,当程序控制流到达这个变量定义式时就得承受构造成本。 离开作用域时就得承受析构成本 即使变量最终并未被使用。
#include <iostream>
#include <string>
constexpr int MinimumPasswordLength = 10;
std::string encryptPassword(const std::string& password)
{
using namespace std;
std::string encrypted;
if(password.length()<MinimumPasswordLength)
{
throw logic_error("Password is too short");
}
//..
return encrypted;
}
int main()
{
return 0;
}
下面方式更好
#include <iostream>
#include <string>
constexpr int MinimumPasswordLength = 10;
std::string encryptPassword(const std::string& password)
{
using namespace std;
if(password.length()<MinimumPasswordLength)
{
throw logic_error("Password is too short");
}
std::string encrypted;
//..
return encrypted;
}
int main()
{
return 0;
}
例如下面的循环场景
// 方法A
;
Widget wfor(int i=0;i<n;i++)
{
// w=i;
}
// 方法B
for(int i=0;i<n;++i)
{
(i);
Widget w}
A:1个构造函数+1个析构函数+n个赋值操作 B:n个构造函数+n个构造函数
具体用那种方法,要根据具体情况评估。
回顾C风格的转型
(T)expression // 将expression转型为T
(expression) // 将expression转型为T T
两种形式并无差别,存粹是小括号的摆放位置不同而已。称此为旧式转型 old-style casts.
C++还提供了四种新式转型
const_cast<T>(expression)
dynamic_cast<T>(expression)
reinterpret_cast<T>(expression)
static_cast<T>(expression)
class A
{
public:
int n1;
int n2;
};
class B
{
public:
()
B{
= make_shared<A>();
a }
inline int &Getn1() const // 这里封装性被破坏,外部可以修改n1
{
return a->n1;
}
inline int &Getn2() const // 这里封装性被破坏,外部可以修改n2
{
return a->n2;
}
private:
std::shared_ptr<A> a;
};
虽然Getn1与Getn2有限制const,但是返回值不是const的
class B
{
public:
()
B{
= make_shared<A>();
a }
inline const int &Getn1() const
{
return a->n1;
}
inline const int &Getn2() const
{
return a->n2;
}
private:
std::shared_ptr<A> a;
};
还有一种是,返回了随时可能被销毁的资源
#include <iostream>
#include <memory>
using namespace std;
class A
{
public:
(int n1, int n2) : n1(n1), n2(n2) {};
Aint n1;
int n2;
};
class B
{
public:
(int n1, int n2)
B{
= make_shared<A>(n1, n2);
a }
inline const int &Getn1() const
{
return a->n1;
}
inline const int &Getn2() const
{
return a->n2;
}
void destoryA()
{
.reset();
a}
private:
std::shared_ptr<A> a;
};
int main(int argc, char **argv)
{
(1, 2);
B bconst int &n1 = b.Getn1();
const int &n2 = b.Getn2();
std::cout << n1 << " " << n2 << std::endl; // 1 2
.destoryA();
b
*const_cast<int *>(&n1) = 100;
// Segmentation fault (core dumped)
std::cout << b.Getn1() << std::endl;
return 0;
}
即使函数在执行中抛出异常,程序的状态仍然是有效的(不会造成资源泄漏或数据结构损坏),但可能无法保证状态是原始状态。
示例:资源的简单释放
假设一个函数在处理动态内存分配,尽管抛出异常,也确保释放了已分配的资源。
#include <iostream>
#include <stdexcept>
void basicGuaranteeExample() {
int* resource = new int[10]; // 动态分配资源
try {
// 模拟某些可能抛出异常的操作
throw std::runtime_error("Something went wrong!");
} catch (...) {
// 捕获异常并确保释放资源
delete[] resource;
std::cout << "Basic guarantee: Resource cleaned up." << std::endl;
throw; // 重新抛出异常
}
}
结果:即使函数抛出异常,动态分配的资源仍然会被释放,程序状态有效。
函数提供强烈保证:若函数抛出异常,程序的状态会回到调用函数之前的状态,不会有任何更改。 可以通过 copy-and-swap 技术实现。
示例:swap 技术在容器的异常安全中应用
#include <vector>
#include <stdexcept>
class ExceptionSafeVector {
std::vector<int> data;
public:
void addElement(int element) {
// 创建临时副本,保证异常发生时不会修改原数据
std::vector<int> temp(data);
.push_back(element); // 操作可能抛出异常
temp
// 若无异常,完成 swap 替换
.swap(temp); // 强烈保证:要么成功,要么不修改 data
data}
void print() const {
for (int val : data) {
std::cout << val << " ";
}
std::cout << std::endl;
}
};
int main() {
;
ExceptionSafeVector vectry {
.addElement(1);
vec.addElement(2);
vec.addElement(3); // 无异常时更新数据
vec} catch (...) {
std::cout << "Strong guarantee: No changes to the original state!" << std::endl;
}
.print(); // 打印已添加的元素
vec}
结果:如果 addElement 抛出异常,data 保持调用前的状态。
函数绝对不会抛出异常,通常用于析构函数或一些保证不会失败的操作。
示例:提供强制的 noexcept 保证
#include <iostream>
#include <utility>
class NoThrowClass {
int* resource;
public:
() : resource(new int(42)) {}
NoThrowClass
// noexcept 保证函数不会抛出异常
~NoThrowClass() noexcept {
delete resource; // 确保资源被释放,不抛异常
}
// noexcept 移动操作:在发生资源转移时,确保不抛异常
(NoThrowClass&& other) noexcept : resource(nullptr) {
NoThrowClass= other.resource;
resource .resource = nullptr;
other}
// 赋值操作符 noexcept
& operator=(NoThrowClass&& other) noexcept {
NoThrowClassif (this != &other) {
delete resource; // 释放已有资源
= other.resource;
resource .resource = nullptr;
other}
return *this;
}
};
int main() {
;
NoThrowClass obj1= std::move(obj1); // 不抛异常
NoThrowClass obj2
std::cout << "No-throw guarantee: Successful resource transfer without exceptions!" << std::endl;
}
结果:析构函数和移动操作通过 noexcept 保证不会抛出异常。
假设有三个函数 A、B 和 C,它们分别提供不同级别的异常安全保证:
inline函数,动作像函数,比宏好用得多,可以调用它们又不需要蒙受函数调用所招致得额外开销。 没有白吃得午餐,inline函数背后得整体观念是,将“对此函数得每一个调用”都以函数本体替换之。一台内存有限得机器上,过度热衷inlining会造成程序体积太大(对可用空间而言)。即使拥有虚内存,inline造成的代码膨胀亦会导致额外得换页行为,降低指令高速缓存装置得击中率,带来效率损失。
class Person
{
public:
...
int age() const { return theAge; } // 一个隐喻的inline申请
...
private:
int theAge;
};
使用inline函数的函数指针,并不是inlined。
inline void f() {...}
void (*pf)() = f;
(); // 是inline调用
f(); // 不是inline调用 pf
构造函数和析构函数往往是inlining的糟糕候选人。
class Example {
int a;
double b;
std::string c;
public:
() : a(0), b(0.0), c("default") {} // 构造函数看似简单,但实际上初始化了多个成员
Example};
即使这个构造函数只有一行,实际上它要调用 std::string 的构造函数,还可能涉及动态内存分配。将这样的函数内联会生成大量代码,增加可执行文件的大小(代码膨胀)。
如A.h内定义了class A, class A中使用了 class B和class C,class B定义在 B.h class C在 C.h,如果 A.h include了B.h C.h,那么修了B.h或者C.h A的实现需要重新编译,因为A.h也发生了变化。所有可以在A.h中使用前置声明。
// A.h
#include "B.h"
#include "C.h"
class A{
public:
(B&b,C&c);
A};
上面代码依存密切,容易引起编译灾难。
// A.h
class B;
class C;
class A{
public:
(B&b,C&c);
A};
想要使用某个类型就需要知道分配多少内存,如
int main()
{
int x; // 定义一个int
; // 定义一个Person对象,需要include到Person的定义式,不然编译不过
Person p...
}
有一种 pointer to implementation的骚操作。
// A.h
class AImpl;
class B;
class C;
class A
{
public:
(B&b, C&c);
A// ...
private:
std::shared_ptr<AImpl> pImpl;
};
这样的设计之下,Person的客户完全与A,B以及A的实现细目分离了。include了A.h的cpp,即使B.h C.h修改了东西,也不会令include A.h的源代码重新编译。
注意,当你声明一个函数而它用到某个class时,你并不需要该class的定义;纵使函数以byvalue方式传递该类型的参数(或返回值)亦然:
// A.h
#include "Bfwd.h"
#include "Cfwd.h"
();
B bfuncvoid showC(C c);
#include "A.h"
#include "AImpl.h"
::A(const B&b,const C&c,const std::string&str):AImpl(new AImpl(b,c,str))
A{}
std::string A::name() const
{
return pImpl->name();
}
三种继承方式
class Animal {
public:
virtual void makeSound() { std::cout << "某种声音" << std::endl; }
protected:
int age;
private:
std::string dna;
};
class Dog : public Animal {
// Animal的public成员在Dog中仍然是public
// Animal的protected成员在Dog中仍然是protected
// Animal的private成员在Dog中不可访问
};
class Engine {
public:
void start() { std::cout << "引擎启动" << std::endl; }
void stop() { std::cout << "引擎停止" << std::endl; }
protected:
void inject_fuel() {}
private:
void internal_process() {}
};
class Car : private Engine { // private继承
// Engine的public和protected成员在Car中变成private
// Engine的private成员在Car中不可访问
public:
void start_car() {
(); // 可以访问Engine的方法,但只能在Car的内部使用
start}
};
int main() {
;
Car car// car.start(); // 错误:Engine的方法对外不可见
.start_car(); // 正确:通过Car的公共接口访问
car}
class Base {
public:
void publicFunc() {}
protected:
void protectedFunc() {}
private:
void privateFunc() {}
};
class Derived : protected Base {
// Base的public成员在Derived中变成protected
// Base的protected成员在Derived中保持protected
// Base的private成员在Derived中不可访问
};
class Further : public Derived {
void func() {
(); // 可以访问,因为在Derived中是protected
publicFunc(); // 可以访问,因为在Derived中是protected
protectedFunc// privateFunc(); // 不能访问
}
};
如果你令 class D以public形式继承class B,那么当你看到class D的某个对象时,你便知道它也是个class B。反之不成立。
我来用一个简单的例子来解释”public继承”中的is-a关系。
class Animal {
public:
virtual void makeSound() {
std::cout << "动物发出声音" << std::endl;
}
void eat() {
std::cout << "动物在进食" << std::endl;
}
};
class Dog : public Animal { // Dog "是一个" Animal
public:
void makeSound() override { // 重写基类的方法
std::cout << "汪汪汪!" << std::endl;
}
void fetchBall() {
std::cout << "狗狗去捡球" << std::endl;
}
};
int main() {
;
Dog dog* animal_ptr = &dog; // 这是合法的,因为 Dog "是一个" Animal
Animal
// 下面这些操作都是合法的
.makeSound(); // 输出:"汪汪汪!"
dog.eat(); // 输出:"动物在进食"
dog
->makeSound(); // 输出:"汪汪汪!"
animal_ptr->eat(); // 输出:"动物在进食"
animal_ptr}
让我解释这个例子:
Animal
是基类,定义了所有动物共有的行为:makeSound()
和
eat()
。
Dog
通过 public
继承自
Animal
,这表明”狗是一个动物”(is-a关系)。
makeSound
和
eat
)makeSound
)fetchBall
)因为存在 is-a 关系:
Animal
的地方都可以使用 Dog
Dog
对象的指针赋值给 Animal
指针(向上转型)Animal
的操作都适用于 Dog
这就是为什么说”适用于基类的每一件事情一定也适用于派生类”。因为派生类对象在任何情况下都可以被当作基类对象来使用,这是 public 继承的核心特性。
相反,如果这种关系不成立,就不应该使用 public 继承。例如,“房子有一个门”这种关系就不应该用 public 继承,而应该用组合(composition)来实现,因为门不是房子的一种类型。
C++有种现象为名字遮掩。在C++中,当派生类声明了一个与基类同名的函数时,基类中所有同名函数(无论参数如何)都会被遮掩,只要原因有:
名称查找规则:
#include <iostream>
class Base
{
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
virtual ~Base();
};
::~Base() {}
Basevoid Base::mf1(int x)
{
std::cout << "Base::mf1(int x)" << std::endl;
}
void Base::mf2()
{
std::cout << "Base::mf2()" << std::endl;
}
void Base::mf3()
{
std::cout << "Base::mf3()" << std::endl;
}
void Base::mf3(double x)
{
std::cout << "Base::mf3(double x)" << std::endl;
}
class Derived : public Base
{
public:
virtual void mf1();
void mf3();
void mf4();
};
void Derived::mf1()
{
std::cout << "Derived::mf1()" << std::endl;
}
void Derived::mf3()
{
std::cout << "Derived::mf3()" << std::endl;
}
void Derived::mf4()
{
std::cout << "Derived::mf4()" << std::endl;
}
int main()
{
;
Derived d.mf1(); // Derived::mf1()
d// d.mf1(12); // 错误:Derived::mf1(int) 遮掩了 Base::mf1(int)
.mf2(); // Base::mf2()
d.mf3(); // Derived::mf3()
d// d.mf3(1);// 错误:Derived::mf3遮掩了Base::mf3
return 0;
}
解决方法
class Derived : public Base
{
public:
using Base::mf1; // 在Base class内名为mf1和mf3的所有东西
using Base::mf3; // 在Derived作用域内都可看见 并且public
virtual void mf1();
void mf3();
void mf4();
};
;
Derived d.Base::mf1(12); d
// 不好的方式:using声明在private继承中
class Derived : private Base {
private:
using Base::mf1; // 这样无法将mf1暴露为public接口
};
// 好的方式:使用转交函数
class Derived : private Base {
public:
// 可以选择性地只转发某些重载版本
void mf1() { Base::mf1(); } // 显式转发到基类版本
void mf1(int x) { Base::mf1(x); } // 可以控制访问级别为public
};
函数接口(function interfaces)继承和函数实现(function implementations)继承.
class Shape
{
public:
virtual void draw() const = 0; // pure-virtual函数使得Shape成为抽象类
virtual void error(const std::string &msg); // impure-virtual函数,提供缺省实现
int objectID() const; // non-virtual函数,强制实现
//...
};
void Shape::error(const std::string &msg)
{
std::cerr << msg << std::endl;
}
int Shape::objectID() const
{
return 0;
}
class Rectangle : public Shape
{
//...
virtual void draw() const {}
};
class Ellipse : public Shape
{
//...
virtual void draw() const {}
};
pure virtual函数有两个最突出的特性:它们必须被任何”继承了它们”的具象class重新声明,而且它们在抽象class中通常没有定义.
声明一个pure virtual函数的目的是为了让derived classes只继承函数接口. 令人意外的是,可以为pure virtual函数提供定义,C++并不会发出怨言,虽然提供了pure virtual函数的定义但是其derived classes仍然要重新override,在derived classes中定义中可以再调用BaseClass的pure virtual的定义实现.
#include <iostream>
class Shape
{
public:
virtual void draw() const = 0;
virtual void error(const std::string &msg);
int objectID() const;
//...
};
void Shape::draw() const
{
std::cerr << "Shape::draw" << std::endl;
}
void Shape::error(const std::string &msg)
{
std::cerr << msg << std::endl;
}
int Shape::objectID() const
{
return 0;
}
class Rectangle : public Shape
{
//...
virtual void draw() const override
{
std::cerr << "Rectangle::draw" << std::endl;
}
};
class Ellipse : public Shape
{
//...
virtual void draw() const override
{
std::cerr << "Ellipse::draw" << std::endl;
}
};
class Circle : public Shape
{
virtual void draw() const override
{
::draw();
Shape}
};
int main()
{
;
Rectangle rect;
Ellipse elli;
Circle circle*ps = ▭
Shape ->draw(); // Rectangle::draw 虽然draw在Circle中是private的,但是Circle继承了Shape的draw函数,所以可以调用
ps->Shape::draw(); // Shape::draw
ps= &elli;
ps ->draw(); // Ellipse::draw
ps->Shape::draw(); // Shape::draw
ps.draw(); // 编译错误: 因为draw为class Circle内draw默认为private的, 如果是struct则是public的
circle// 为circle的draw函数设置public 则会输出 Shape::draw
return 0;
}
声明简朴的(非纯)impure virtual函数的目的,是让derived classes继承该函数的接口和缺省实现,也就是选择性实现,而且能够实现多态效果,如果必须要derived classes自己实现就使用纯虚函数.
声明non-virtual函数的目的是为了令derived classes继承函数的接口及一份强制性实现。当然non-virtual可以写出自己的实现但是无法实现多态效果.
#include <iostream>
class Shape
{
public:
int objectID() const;
//...
};
int Shape::objectID() const
{
return 0;
}
class Ellipse : public Shape
{
public:
// 坚决 绝不重新定义继承而来的non-virtual函数
// 容易惹祸上身
int objectID() const
{
return 1;
}
};
int main()
{
;
Ellipse elli*ps = &elli;
Shape std::cout << ps->objectID() << std::endl; // 0
std::cout << elli.objectID() << std::endl; // 1
return 0;
}
不一定使用virtual函数才能展现出多态效果。
NVI手法,通过public non-virtual成员函数间接调用private virtual函数,称为 non-virtual interface (NVI)手法。
下面代码可以防止Derived外部调用虚函数doSomethingImpl
#include <iostream>
class Base
{
public:
// public 非虚函数作为接口
void doSomething()
{
// 前置处理
();
preProcess
// 调用私有虚函数
();
doSomethingImpl
// 后置处理
();
postProcess}
private:
// private 虚函数实现具体行为
virtual void doSomethingImpl() = 0;
virtual void preProcess() { /* 默认实现 */ }
virtual void postProcess() { /* 默认实现 */ }
};
class Derived : public Base
{
private:
// 重写私有虚函数
virtual void doSomethingImpl() override
{
// 派生类的具体实现
std::cout << "Derived::doSomethingImpl" << std::endl;
}
};
int main()
{
;
Derived d&b = d;
Base .doSomething(); // Derived::doSomethingImpl
b
return 0;
}
再来个更符合实际的例子
class Document {
public:
void save() {
// 统一的保存前检查
if (!canSave()) {
throw std::runtime_error("无法保存文档");
}
// 加锁
std::lock_guard<std::mutex> lock(mutex_);
// 调用具体的保存实现
();
saveImpl
// 更新保存状态
lastSaveTime_ = std::time(nullptr);
}
private:
virtual void saveImpl() = 0;
virtual bool canSave() const { return true; }
std::mutex mutex_;
std::time_t lastSaveTime_;
};
class TextDocument : public Document {
private:
virtual void saveImpl() override {
// 实现文本文档的具体保存逻辑
}
};
NVI手法对publicvirtual函数而言是一个有趣的替代方案, 还有一种方式使用函数指针,由Function Pointers实现 Strategy 模式。
#include <iostream>
class Base;
void doSomethingImpl1(const Base &b)
{
std::cout << "doSomethingImpl1" << std::endl;
}
void doSomethingImpl2(const Base &b)
{
std::cout << "doSomethingImpl2" << std::endl;
}
class Base
{
public:
typedef void (*DoSomethingImplFunc)(const Base &b);
explicit Base(DoSomethingImplFunc func = doSomethingImpl1) : funcImpl(func)
{
}
// public 非虚函数作为接口
void doSomething()
{
// 前置处理
();
preProcess
// 调用私有虚函数
(*this);
funcImpl
// 后置处理
();
postProcess}
private:
virtual void preProcess() { /* 默认实现 */ }
virtual void postProcess() { /* 默认实现 */ }
private:
;
DoSomethingImplFunc funcImpl};
class Derived : public Base
{
public:
explicit Derived(Base::DoSomethingImplFunc func) : Base(func)
{
}
};
int main()
{
(&doSomethingImpl1);
Derived d1(&doSomethingImpl2);
Derived d2&b1 = d1;
Base &b2 = d2;
Base .doSomething(); // doSomethingImpl1
b1.doSomething(); // doSomethingImpl2
b2.doSomething(); // doSomethingImpl1
d1.doSomething(); // doSomethingImpl2
d2return 0;
}
可以将上面的
typedef void (*DoSomethingImplFunc)(const Base &b);
改为std::function
将会有很多优势。
#include <iostream>
#include <functional>
class Base;
void doSomethingImpl1(const Base &b)
{
std::cout << "doSomethingImpl1" << std::endl;
}
void doSomethingImpl2(const Base &b)
{
std::cout << "doSomethingImpl2" << std::endl;
}
class Base
{
public:
typedef std::function<void(const Base &b)> DoSomethingImplFunc;
explicit Base(DoSomethingImplFunc func = doSomethingImpl1) : funcImpl(func)
{
}
// public 非虚函数作为接口
void doSomething()
{
// 前置处理
();
preProcess
// 调用私有虚函数
(*this);
funcImpl
// 后置处理
();
postProcess}
private:
virtual void preProcess() { /* 默认实现 */ }
virtual void postProcess() { /* 默认实现 */ }
private:
;
DoSomethingImplFunc funcImpl};
class Derived : public Base
{
public:
explicit Derived(Base::DoSomethingImplFunc func) : Base(func)
{
}
};
struct StructDoSomethingImpl
{
void operator()(const Base &b) const
{
std::cout << "StructDoSomethingImpl::operator()" << std::endl;
}
};
class ClassDoSomthingImpl
{
public:
void Do(const Base &b) const
{
std::cout << "ClassDoSomthingImpl" << std::endl;
}
};
int main()
{
(&doSomethingImpl1);
Derived d1(&doSomethingImpl2);
Derived d2&b1 = d1;
Base &b2 = d2;
Base .doSomething(); // doSomethingImpl1
b1.doSomething(); // doSomethingImpl2
b2.doSomething(); // doSomethingImpl1
d1.doSomething(); // doSomethingImpl2
d2
// 使用函数对象
((StructDoSomethingImpl())); // StructDoSomethingImpl::operator()
Derived d3.doSomething();
d3
// 使用成员函数
;
ClassDoSomthingImpl classDoSomethingImpl(std::bind(&ClassDoSomthingImpl::Do, classDoSomethingImpl, std::placeholders::_1));
Derived d4.doSomething(); // ClassDoSomthingImpl
d4
return 0;
}
上面基本都是设计模式骚操作。
non-virtual函数是静态绑定的,virtual函数是动态绑定的。
#include <iostream>
class A
{
public:
void func()
{
std::cout << "A::func" << std::endl;
}
};
class B : public A
{
public:
int func()
{
std::cout << "B::func" << std::endl;
return 0;
}
};
int main()
{
;
B bObj*pA = &bObj;
A *pB = &bObj;
B ->func(); // A::func
pA->func(); // B::func
pB->A::func(); // A::func
pB->B::func(); // B::func
pB
return 0;
}
上面的写法非常模糊,使用的时候还得区别我们调用的到底是基类的定义还是派生类中方法的定义。
你只能继承两种函数:virtual和non-virtual函数,重新定义一个继承而来的non-virtual函数永远是错误的。
#include <iostream>
class A
{
public:
enum class Color
{
= 0,
RED = 1
GREEN };
virtual void func(Color color = Color::RED)
{
std::cout << "A::func " << (int)color << std::endl;
}
};
class B : public A
{
public:
virtual void func(Color color) // 虽然可以指定默认实参但不要这样做 非常混乱 是跟着静态类型走的
{
std::cout << "B::func " << (int)color << std::endl;
}
};
int main()
{
;
B bObj*pA = &bObj;
A *pB = &bObj;
B ->func(); // 通过A*调用func有默认值可使用 B::func 0
pA// pB->func(); // 通过B*调用func需要提供color实参
->func(B::Color::GREEN); // B::func 1
pB
return 0;
}
在应用域(has-a 关系) 这表示一个对象包含另一个对象作为其成员。例如:
// 应用域的复合示例
class Person {
private:
; // Person has-a Address
Address address; // Person has-a PhoneNumber
PhoneNumber phone};
这里的关系很直观:一个人”有一个”地址和”有一个”电话号码。
在实现域(is-implemented-in-terms-of 关系) 这表示一个类使用另一个类来实现其功能。例如:
// 实现域的复合示例
class Set {
private:
<T> list; // Set is-implemented-in-terms-of List
List// 使用List来实现Set的功能
};
public继承表示 is-a(是一个)关系:
class Animal {
public:
virtual void makeSound() = 0;
};
class Dog : public Animal { // Dog is-a Animal
public:
void makeSound() override { /* 汪汪 */ }
};
主要区别:
使用建议:
这样的理解和使用可以帮助我们创建更灵活、更易维护的代码。
基类的private virtual是可以被覆写的,并且是动态绑定
// 基类的private virtual是可以被覆写的,并且是动态绑定
#include <iostream>
class A
{
private:
virtual void func()
{
std::cout << "A::func " << std::endl;
}
public:
void exe()
{
();
func}
};
class B : public A
{
public:
virtual void func() override
{
std::cout << "B::func " << std::endl;
}
void exe()
{
::exe();
A}
};
int main()
{
;
B bObj*pA = &bObj;
A *pB = &bObj;
B ->exe(); // B::func
pA->exe(); // B::func
pB
return 0;
}
private继承主要有两条规则
#include <iostream>
#include <functional>
class Timer
{
public:
()
Timer{
std::cout << "Timer::Timer()" << std::endl;
auto callback = std::bind(&Timer::OnTick, this);
();
callback// 完全可以把callback注册到某处去 这让只要使用派生类对象就好了关注上层业务
}
protected:
virtual void OnTick()
{
std::cout << "Timer::OnTick " << std::endl;
}
};
class Widget : private Timer
{
private:
virtual void OnTick() override
{
std::cout << "Widget::OnTick " << std::endl;
();
UpdateDisplay}
void UpdateDisplay()
{
}
};
int main()
{
// 创建对象时完全可以让Timer内进行一些操作将自己挂载到某处
;
Widget widgetObject
// Timer::Timer()
// Timer::OnTick
return 0;
}
能用复合就用复合,下面就是一个骚操作
#include <iostream>
class A
{
public:
virtual void OnTick()
{
std::cout << "A::OnTick" << std::endl;
}
};
class B
{
private:
class BA : public A
{
public:
virtual void OnTick() override
{
std::cout << "BA::A" << std::endl;
}
};
;
BA bCompositionA};
int main()
{
;
B bObj
return 0;
}
private继承相比组合而言可以进行孔基类优化
// 场景二:空基类优化
class EmptyClass {
typedef int DataType; // 只包含类型定义
};
// 使用复合
class BadWidget {
private:
; // 会占用内存
EmptyClass ecint data; // 4字节
}; // sizeof(BadWidget) > 4
// 使用private继承
class GoodWidget : private EmptyClass {
private:
int data; // 4字节
}; // sizeof(GoodWidget) == 4,实现了空基类优化
#include <iostream>
#include <functional>
class A
{
public:
void func()
{
std::cout << "A::func" << std::endl;
}
};
class B
{
public:
bool func()
{
std::cout << "B::func" << std::endl;
return true;
}
};
class C : public A, public B
{
};
int main()
{
;
C c// c.func(); //"C::func" is ambiguous
.A::func(); // A::func
c.B::func(); // B::func
creturn 0;
}
菱形多重继承问题
#include <iostream>
#include <functional>
class A
{
public:
void func()
{
std::cout << "A::func " << n++ << std::endl;
}
private:
int n{0};
};
class B : public A
{
};
class C : public A
{
};
class D : public B, public C
{
};
int main()
{
;
D d// d.func(); // "D::func" is ambiguous
.B::func(); // A::func 0
d.C::func(); // A::func 0
d.B::func(); // A::func 1
d.C::func(); // A::func 1
dreturn 0;
}
上面的d对象其实存在两个int n。使用virtual继承
#include <iostream>
#include <functional>
class A
{
public:
void func()
{
std::cout << "A::func " << n++ << std::endl;
}
private:
int n{0};
};
class B : virtual public A
{
};
class C : virtual public A
{
};
class D : public B, public C
{
};
int main()
{
;
D d.func(); // A::func 0
d.B::func(); // A::func 1
d.C::func(); // A::func 2
d.B::func(); // A::func 3
d.C::func(); // A::func 4
dreturn 0;
}
下面就是非常烂的行为
#include <iostream>
#include <functional>
class A
{
public:
void func()
{
std::cout << "A::func " << n++ << std::endl;
}
virtual void exe()
{
std::cout << "A::exe" << std::endl;
}
private:
int n{0};
};
class B : virtual public A
{
public:
virtual void exe() override
{
std::cout << "B::exe" << std::endl;
}
};
class C : virtual public A
{
public:
virtual void exe() override
{
std::cout << "C::exe" << std::endl;
}
};
class D : public B, public C
{
public:
virtual void exe() override
{
std::cout << "D::exe" << std::endl;
}
};
int main()
{
;
D d.exe(); // D::exe
d*c = &d;
C ->exe(); // D::exe
c*b = &d;
B ->exe(); // D::exe
b*a = &d;
A ->exe(); // D::exe
areturn 0;
}
C++ template机制自身是一部完整的图灵机,它可以被用来计算任何可计算的值,于是导出了模板元编程,创造出 在C++编译器内执行并于编译完成时停止执行的 程序。
面向对象编程世界总是以显式接口和运行期多态解决问题。
#include <iostream>
using namespace std;
template <typename T>
void doProcessing(T &w)
{
if (w.size() > 10 && w != someNastyWidget)
{
(w);
T temp.normalize();
temp.swap(w);
temp}
}
int main(int argc, char **argv)
{
return 0;
}
w必须支持哪一种接口,看起来类型T好像必须支持size,normalize,swap成员函数,copy构造函数,不等比较。 这一组表达式(对此template而言必须有效编译)便是T必须支持的一组隐式接口。
凡涉及w的任何函数调用,例如operator >和 operator!=,有可能造成template具现化,使这些调用得以成功。这样的具现行为发生在编译期。 “以不同的template参数具现化function templates“会导致调用不同的函数,这便是所谓的编译期多态(compile-timepolymorphism)。
template<class T> class Widget; // 使用 class
template<typename T> class Widget; // 使用 typename
上面的用法,class与typename意义完全相同。但是C++并不总是把class和typename视为等价,有时候一定得使用typename。
template<typename C>
void print2nd(const C& container)
{
::const_iterator* x;
C// ...
}
上面代码片段,在我们知道C是什么之前,没有任何办法可以知道C::const_iterator是否为一个类型。编译器遇见这种情况会假设为非类型。直接告诉编译器就能解决。
template<typename C>
void print2nd(const C& container)
{
typename C::const_iterator iter(container.begin());
// ...
}
template<typename C>
void f(const C& container, typename C::iterator iter)
{
}
但是是存在例外得,在基类列表和成员初值列中不能使用 typename
#include <iostream>
template <typename T>
class Base
{
public:
() {}
Baseclass Nested
{
public:
() {}
Nested};
};
template <typename T>
class Derived : public Base<T>::Nested
{ // 不能使用 typename 作为基类修饰符
public:
() : Base<T>::Nested()
Derived{ // 不能使用 typename 作为成员初值列修饰符
// ... 其他代码 ...
typename Base<T>::Nested n; // 必须使用 typename 作为成员修饰符
typedef typename Base<T>::Nested NestedType; // 必须使用 typename 作为类型修饰符
;
NestedType n2}
};
int main()
{
<int> d;
Derivedreturn 0;
}
this->
指针通过this->
明确指示成员属于基类模板,this是派生类的指针,this->
可以用来访问基类模板的成员。
#include <iostream>
using namespace std;
template <typename T>
class Base
{
public:
() { cout << "Base()" << endl; }
Basevirtual ~Base() { cout << "~Base()" << endl; }
void display()
{
<< "value = " << value << endl;
cout }
protected:
= 42;
T value };
template <typename T>
class Derived : public Base<T>
{
public:
() { cout << "Derived()" << endl; }
Derived~Derived() { cout << "~Derived()" << endl; }
void show()
{
this->display();
// display();
// error: there are no arguments to ‘display’ that depend on a template parameter, so a declaration of ‘display’ must be available [-fpermissive]
}
};
int main(int argc, char **argv)
{
<int> d;
Derived.show();
dreturn 0;
}
在派生类中使用using也是可以的
template <typename T>
class Derived : public Base<T>
{
public:
() { cout << "Derived()" << endl; }
Derived~Derived() { cout << "~Derived()" << endl; }
using Base<T>::display;
void show()
{
();
display}
};
通过明确写出基类的名称来访问基类模板的成员。
template <typename T>
class Derived : public Base<T>
{
public:
() { cout << "Derived()" << endl; }
Derived~Derived() { cout << "~Derived()" << endl; }
void show()
{
<T>::display();
Base}
};
使用this->是一种更通用的方式,尤其是在派生类中需要频繁访问基类成员时,使用基类资格修饰符可以显式地表明成员来自基类,但代码可能显得冗长。
背景
模板在 C++ 中非常强大,但它们也可能导致代码膨胀,因为每个模板实例化都会生成一份独立的代码。如果模板中包含了与模板参数无关的代码,这些代码会被重复生成,导致不必要的代码冗余。
核心思想
将与模板参数无关的代码从模板中分离出来,放到非模板的普通函数或类中。这样可以避免重复生成这些代码,从而减少代码膨胀。
示例
假设我们有一个模板类,其中有一些与模板参数无关的逻辑:
#include <string>
#include <iostream>
template<typename T>
class Printer {
public:
void print(const T& value) const {
std::cout << "Value: " << value << std::endl; // 与模板参数无关 这里的字符串常量其实是占据空间的
}
};
在这个例子中,std::cout << "Value: "
是与模板参数无关的代码,但它会在每个模板实例化中重复生成。
改进方法
我们可以将与模板参数无关的部分抽离到一个非模板的辅助函数中:
#include <string>
#include <iostream>
// 非模板辅助函数
void printImpl(const std::string& value) {
std::cout << "Value: " << value << std::endl;
}
template<typename T>
class Printer {
public:
void print(const T& value) const {
(std::to_string(value)); // 调用非模板函数
printImpl}
};
通过这种方式,printImpl 只会生成一份代码,而不会因为模板实例化而重复生成。
优势
减少代码膨胀:与模板参数无关的代码只会生成一次。
提高可读性和可维护性:将通用逻辑集中在一个地方,便于修改和调试。
分离关注点:模板只负责与模板参数相关的逻辑,非模板代码处理通用逻辑。
非类型模板参数(如整数、指针等)也会导致代码膨胀,因为每个不同的非类型参数都会实例化一个新的模板版本。可以通过将非类型模板参数替换为函数参数或类的成员变量来避免这种膨胀。
// 非类型模板参数导致代码膨胀
template<int N>
class Array {
public:
void printSize() const {
std::cout << "Size: " << N << std::endl;
}
};
// 改进:将非类型模板参数替换为成员变量
class Array {
public:
(int n) : size(n) {}
Arrayvoid printSize() const {
std::cout << "Size: " << size << std::endl;
}
private:
int size;
};
// 通过这种方式,Array 类只会生成一份代码,而不需要为每个不同的 N 生成新的模板实例。
模板类型参数(如 int、float 等)也会导致代码膨胀,尤其是当多个类型的二进制表示相同时(例如 int 和 unsigned int 在某些平台上可能有相同的二进制表示)。可以通过共享实现代码来减少膨胀。
// 原始模板,可能导致代码膨胀
template<typename T>
class Calculator {
public:
(T a, T b) {
T addreturn a + b;
}
};
// 改进:通过类型萃取(type traits)共享实现
template<typename T>
class CalculatorImpl {
public:
(T a, T b) {
T addreturn a + b;
}
};
template<typename T>
class Calculator : public CalculatorImpl<typename std::conditional<std::is_integral<T>::value, int, double>::type>
{
};
// 改进的核心思想:
// 类型萃取(type traits):
// 使用 std::conditional 和 std::is_integral 来判断模板参数 T 的类型特性。
// 如果 T 是整数类型(如 int、unsigned int),则统一使用 int。
// 如果 T 是浮点类型(如 float、double),则统一使用 double。
// 共享实现:
// 将实际的实现逻辑(add 函数)放在 CalculatorImpl 中。
// Calculator 根据 T 的类型特性选择一个共享的实现(int 或 double)。
如何使用成员函数模板来实现“接受所有兼容类型”的函数,解释为什么在某些情况下仍需要显式声明普通的拷贝构造函数和拷贝赋值操作符。
核心内容总结:
使用成员函数模板实现泛化构造和赋值
#include <iostream>
#include <string>
class MyClass {
public:
// 普通构造函数
(const std::string& str) : data(str) {}
MyClass
// 泛化的拷贝构造函数(成员函数模板)
template<typename T>
(const T& other) : data(std::to_string(other)) {
MyClassstd::cout << "Generic constructor called with: " << data << std::endl;
}
// 泛化的赋值操作符(成员函数模板)
template<typename T>
& operator=(const T& other) {
MyClass= std::to_string(other);
data std::cout << "Generic assignment operator called with: " << data << std::endl;
return *this;
}
// 显式声明普通的拷贝构造函数
(const MyClass& other) : data(other.data) {
MyClassstd::cout << "Copy constructor called" << std::endl;
}
// 显式声明普通的拷贝赋值操作符
& operator=(const MyClass& other) {
MyClassif (this != &other) {
= other.data;
data std::cout << "Copy assignment operator called" << std::endl;
}
return *this;
}
void print() const {
std::cout << "Data: " << data << std::endl;
}
private:
std::string data;
};
示例代码的使用
int main() {
("Hello"); // 普通构造函数
MyClass obj1.print();
obj1
= 42; // 泛化的构造函数
MyClass obj2 .print();
obj2
= 3.14; // 泛化的赋值操作符
obj1 .print();
obj1
= obj1; // 普通拷贝构造函数
MyClass obj3 .print();
obj3
= obj3; // 普通拷贝赋值操作符
obj2 .print();
obj2
return 0;
}
// 输出结果
// Data: Hello
// Generic constructor called with: 42
// Data: 42
// Generic assignment operator called with: 3.140000
// Data: 3.140000
// Copy constructor called
// Data: 3.140000
// Copy assignment operator called
// Data: 3.140000
代码解析
template<typename T> MyClass(const T& other)
和
template<typename T> MyClass& operator=(const T& other)
是成员函数模板。MyClass(const MyClass& other)
和
MyClass& operator=(const MyClass& other)
是显式声明的普通拷贝构造函数和赋值操作符。核心思想
示例代码
假设我们有一个简单的模板类 Rational,表示有理数:
template<typename T>
class Rational {
public:
(const T& numerator = 0, const T& denominator = 1)
Rational: num(numerator), den(denominator) {}
// 成员函数模板,用于支持加法
template<typename U>
<T> operator+(const Rational<U>& other) const {
Rationalreturn Rational<T>(
* other.den + other.num * den,
num * other.den
den );
}
private:
, den;
T num};
问题:
如果尝试将一个 Rational<int>
与一个
Rational<double>
相加,代码可能会失败,因为成员函数模板不会触发隐式类型转换。 例如
<int> r1(1, 2);
Rational<double> r2(3.5, 4.5);
Rational
// 编译器可能无法解析 r1 + r2
auto result = r1 + r2; // 错误:无法找到合适的 operator+
这是因为 r1 的类型是 Rational<int>
,而 r2 的类型是
Rational<double>
。编译器不会自动将 r1 转换为
Rational<double>
或 r2 转换为
Rational<int>
。
改进:使用非成员函数模板
为了解决上述问题,可以将 operator+
定义为非成员函数模板:
template<typename T>
class Rational {
public:
(const T& numerator = 0, const T& denominator = 1)
Rational: num(numerator), den(denominator) {}
// 声明友元函数模板
template<typename U, typename V>
friend Rational<U> operator+(const Rational<U>& lhs, const Rational<V>& rhs);
private:
, den;
T num};
// 非成员函数模板
template<typename U, typename V>
<U> operator+(const Rational<U>& lhs, const Rational<V>& rhs) {
Rationalreturn Rational<U>(
.num * rhs.den + rhs.num * lhs.den,
lhs.den * rhs.den
lhs);
}
int main() {
<int> r1(1, 2);
Rational<double> r2(3.5, 4.5);
Rational
// 使用非成员函数模板,支持隐式类型转换
auto result = r1 + r2;
// 输出结果
// 注意:这里 result 的类型是 Rational<int>,因为模板参数 U 是 int
return 0;
}
优势:
operator+
可以接受不同类型的 Rational
对象(如 Rational<int>
和
Rational<double>
)。为什么非成员函数模板更好?
核心思想
实例代码
以下是一个简单的 Traits Classes 示例,用于判断一个类型是否是指针类型:
// 默认模板:假设类型不是指针
template<typename T>
struct IsPointer {
static const bool value = false;
};
// 特化版本:如果是指针类型,返回 true
template<typename T>
struct IsPointer<T*> {
static const bool value = true;
};
// 使用示例
#include <iostream>
int main() {
std::cout << IsPointer<int>::value << std::endl; // 输出 0(false)
std::cout << IsPointer<int*>::value << std::endl; // 输出 1(true)
return 0;
}
解释
Traits classes 可以与函数重载结合,在编译期根据类型选择不同的实现。例如:
#include <iostream>
// Traits Classes:判断类型是否是指针
template<typename T>
struct IsPointer {
static const bool value = false;
};
template<typename T>
struct IsPointer<T*> {
static const bool value = true;
};
// 重载函数:针对指针类型
template<typename T>
void process(T* ptr) {
std::cout << "Processing a pointer" << std::endl;
}
// 重载函数:针对非指针类型
template<typename T>
void process(T value) {
std::cout << "Processing a non-pointer" << std::endl;
}
// 使用示例
int main() {
int x = 42;
int* p = &x;
(x); // 调用非指针版本
process(p); // 调用指针版本
process
return 0;
}
解释
Traits classes 还可以结合模板特化和 SFINAE(Substitution Failure Is Not An Error)技术,在编译期实现类似 if-else 的逻辑。例如:
#include <iostream>
// Traits Classes:判断类型是否是指针
template<typename T>
struct IsPointer {
static const bool value = false;
};
template<typename T>
struct IsPointer<T*> {
static const bool value = true;
};
// 编译期 if-else:选择不同的实现
template<typename T>
void process(T value) {
// if constexpr 是 C++17 引入的特性,用于在编译期执行条件判断。
if constexpr (IsPointer<T>::value) {
std::cout << "Processing a pointer" << std::endl;
} else {
std::cout << "Processing a non-pointer" << std::endl;
}
}
// 使用示例
int main() {
int x = 42;
int* p = &x;
(x); // 输出 "Processing a non-pointer"
process(p); // 输出 "Processing a pointer"
process
return 0;
}
Traits Classes的实现方式
优势
核心思想
示例代码:
以下是一个简单的TMP示例,用于计算编译期的阶乘
// TMP 实现阶乘计算
template<int N>
struct Factorial {
static const int value = N * Factorial<N - 1>::value;
};
// 特化模板:递归终止条件
template<>
struct Factorial<0> {
static const int value = 1;
};
// 使用示例
#include <iostream>
int main() {
std::cout << "Factorial of 5: " << Factorial<5>::value << std::endl; // 输出 120
return 0;
}
解释:
Factorial<N>
是一个模板类,通过递归模板实例化计算阶乘。Factorial<0>
是特化模板,用于终止递归。Factorial<5>::value
时,所有计算在编译期完成,运行时只需直接使用结果。TMP可以根据不同的策略组合生成定制化代码
// 策略类
struct PolicyA {
static void execute() {
std::cout << "Policy A executed" << std::endl;
}
};
struct PolicyB {
static void execute() {
std::cout << "Policy B executed" << std::endl;
}
};
// TMP 组合策略
template<typename Policy>
class Strategy {
public:
void run() {
::execute();
Policy}
};
// 使用示例
int main() {
<PolicyA> strategyA;
Strategy.run(); // 输出 "Policy A executed"
strategyA
<PolicyB> strategyB;
Strategy.run(); // 输出 "Policy B executed"
strategyB
return 0;
}
解释
TMP可以在编译期检测类型特征,避免生成对某些类型无效的代码
#include <type_traits>
// TMP 检测类型是否为指针
template<typename T>
struct IsPointer {
static const bool value = false;
};
template<typename T>
struct IsPointer<T*> {
static const bool value = true;
};
// TMP 避免生成不适合的代码
template<typename T>
void process(T value) {
static_assert(!IsPointer<T>::value, "Pointers are not allowed!");
std::cout << "Processing non-pointer type" << std::endl;
}
// 使用示例
int main() {
int x = 42;
(x); // 正常编译
process
int* p = &x;
// process(p); // 编译错误:Pointers are not allowed!
return 0;
}
解释
TMP的局限性
核心内容
std::set_new_handler
设置全局的
new-handler。std::set_new_handler
接受一个函数指针作为参数,该函数就是 new-handler。std::bad_alloc
。示例代码
#include <iostream>
#include <new> // std::set_new_handler
// 自定义 new-handler
void myNewHandler()
{
std::cerr << "Memory allocation failed! Attempting to recover..." << std::endl;
// 尝试释放资源或记录日志
// 如果无法恢复,可以终止程序
std::abort();
}
int main()
{
// 设置全局 new-handler
std::set_new_handler(myNewHandler);
try
{
// 尝试分配大量内存,导致分配失败
int *p = new int[1000000000000];
if (p)
{
delete[] p;
}
}
catch (const std::bad_alloc &e)
{
std::cerr << "Caught exception: " << e.what() << std::endl;
}
return 0;
}
// 输出
// Memory allocation failed! Attempting to recover...
// Aborted (core dumped)
可以为特性的类设置自己的 new-handler,而不影响全局的new-handler,多线程程序绝非下面这样简单的
#include <iostream>
#include <new>
class MyClass {
public:
// 设置类专属的 new-handler
static void myClassNewHandler() {
std::cerr << "MyClass memory allocation failed!" << std::endl;
std::abort();
}
// 重载 operator new
static void* operator new(size_t size) {
std::set_new_handler(myClassNewHandler); // 设置专属 new-handler
void* ptr = ::operator new(size); // 调用全局 operator new
std::set_new_handler(nullptr); // 恢复全局 new-handler
return ptr;
}
};
int main() {
try {
* obj = new MyClass[1000000000000]; // 分配失败时调用类的 new-handler
MyClassdelete[] obj;
} catch (const std::bad_alloc& e) {
std::cerr << "Caught exception: " << e.what() << std::endl;
}
return 0;
}
// MyClass memory allocation failed!
注意事项
std::set_new_handler
会返回当前的 new-handler。std::bad_alloc
。核心内容
std::bad_alloc
,而不是返回 nullptr。示例代码
#include <iostream>
#include <cstdlib>
class MyClass
{
public:
// 自定义 operator new
void *operator new(size_t size)
{
std::cout << "Custom new for MyClass, size: " << size << std::endl;
return std::malloc(size);
}
// 自定义 operator delete
void operator delete(void *ptr)
{
std::cout << "Custom delete for MyClass" << std::endl;
std::free(ptr);
}
};
int main()
{
*obj = new MyClass(); // 调用自定义的 new
MyClass delete obj; // 调用自定义的 delete
return 0;
}
// 输出
// Custom new for MyClass, size: 1
// Custom delete for MyClass
#include <iostream>
#include <cstdlib>
// 全局替换 operator new
void* operator new(size_t size) {
std::cout << "Global new, size: " << size << std::endl;
return std::malloc(size);
}
// 全局替换 operator delete
void operator delete(void* ptr) noexcept {
std::cout << "Global delete" << std::endl;
std::free(ptr);
}
int main() {
int* p = new int(42); // 调用全局 new
delete p; // 调用全局 delete
return 0;
}
// 输出
// Global new, size: 4
// Global delete
在某些特定场景下,可以为特定的需求自定义为new和delete,例如使用内存池优化小对象的分配。
#include <cstdlib>
#include <iostream>
#include <vector>
class MemoryPool {
static const std::size_t POOL_SIZE = 1024; // 内存池大小
char pool[POOL_SIZE]; // 内存池
std::vector<void*> freeBlocks; // 空闲块列表
public:
() {
MemoryPoolfor (std::size_t i = 0; i < POOL_SIZE; i += sizeof(void*)) {
.push_back(&pool[i]);
freeBlocks}
}
void* allocate() {
if (freeBlocks.empty()) {
throw std::bad_alloc();
}
void* block = freeBlocks.back();
.pop_back();
freeBlocksreturn block;
}
void deallocate(void* ptr) {
.push_back(ptr);
freeBlocks}
};
class PooledClass {
static MemoryPool pool; // 静态内存池
public:
// 重载 new 操作符
void* operator new(std::size_t size) {
std::cout << "PooledClass new: allocating " << size << " bytes from pool\n";
return pool.allocate();
}
// 重载 delete 操作符
void operator delete(void* ptr) noexcept {
std::cout << "PooledClass delete: deallocating memory to pool\n";
.deallocate(ptr);
pool}
};
// 定义静态内存池
::pool;
MemoryPool PooledClass
int main() {
* obj1 = new PooledClass; // 从内存池分配
PooledClass* obj2 = new PooledClass; // 从内存池分配
PooledClass
delete obj1; // 释放到内存池
delete obj2; // 释放到内存池
return 0;
}
核心内容总结
规则 1:成对重载 new 和 delete 如果你重载了 new,也必须重载对应的
delete。 如果你重载了数组版本的
new[]
,也必须重载对应的 delete[]
。
#include <cstdlib>
#include <iostream>
class MyClass {
public:
// 重载 new
void* operator new(std::size_t size) {
std::cout << "Custom new: allocating " << size << " bytes\n";
return std::malloc(size);
}
// 重载 delete
void operator delete(void* ptr) noexcept {
std::cout << "Custom delete\n";
std::free(ptr);
}
// 重载 new[]
void* operator new[](std::size_t size) {
std::cout << "Custom new[]: allocating " << size << " bytes\n";
return std::malloc(size);
}
// 重载 delete[]
void operator delete[](void* ptr) noexcept {
std::cout << "Custom delete[]\n";
std::free(ptr);
}
};
int main() {
* obj = new MyClass; // 调用自定义 new
MyClassdelete obj; // 调用自定义 delete
* arr = new MyClass[5]; // 调用自定义 new[]
MyClassdelete[] arr; // 调用自定义 delete[]
return 0;
}
// Custom new: allocating 1 bytes
// Custom delete
// Custom new[]: allocating 20 bytes
// Custom delete[]
为什么需要成对重载? 如果只重载了 new 而没有重载
delete,释放内存时会调用默认的 delete,可能导致未定义行为。
同样,如果只重载了 new[]
而没有重载
delete[]
,会导致内存泄漏或崩溃。
规则 2:确保 new 和 delete 的行为一致 自定义的 new 和 delete 必须使用相同的内存分配和释放机制。 如果 new 使用了 malloc 分配内存,那么 delete 必须使用 free 释放内存。 * 如果 new 使用了自定义的内存池,那么 delete 也必须释放到同一个内存池。
class MyClass {
public:
static void* operator new(std::size_t size) {
std::cout << "Allocating memory using malloc\n";
return std::malloc(size); // 使用 malloc 分配内存
}
static void operator delete(void* ptr) noexcept {
std::cout << "Freeing memory using free\n";
std::free(ptr); // 使用 free 释放内存
}
};
为什么需要行为一致? * 如果 new 和 delete 使用不同的内存管理机制,可能导致内存泄漏或崩溃。
规则 3:处理内存分配失败 自定义的 new
操作符必须在内存分配失败时抛出 std::bad_alloc
异常。
不能返回 nullptr,因为这会违反 C++ 的内存分配语义。
#include <new> // std::bad_alloc
class MyClass {
public:
static void* operator new(std::size_t size) {
void* ptr = std::malloc(size);
if (!ptr) {
throw std::bad_alloc(); // 内存分配失败时抛出异常
}
return ptr;
}
static void operator delete(void* ptr) noexcept {
std::free(ptr);
}
};
为什么需要抛出异常? 如果 new 返回
nullptr,调用者可能不会检查返回值,从而导致程序崩溃。 抛出
std::bad_alloc
是 C++ 的标准行为,符合开发者的预期。
规则 4:避免隐藏默认的 new 和 delete 如果你重载了类级别的 new 和 delete,不要隐藏全局的 new 和 delete。 如果需要调用全局的 new 和 delete,可以使用 ::new 和 ::delete。
class MyClass {
public:
static void* operator new(std::size_t size) {
std::cout << "Custom new\n";
return ::operator new(size); // 调用全局 new
}
static void operator delete(void* ptr) noexcept {
std::cout << "Custom delete\n";
::operator delete(ptr); // 调用全局 delete
}
};
规则 5:为对齐分配提供支持(C++17 及以上) * 如果需要支持对齐分配(aligned allocation),必须重载对齐版本的 new 和 delete。
#include <cstdlib>
#include <iostream>
#include <new>
class MyClass {
public:
static void* operator new(std::size_t size, std::align_val_t alignment) {
std::cout << "Aligned new: alignment = " << static_cast<std::size_t>(alignment) << "\n";
return std::aligned_alloc(static_cast<std::size_t>(alignment), size);
}
static void operator delete(void* ptr, std::align_val_t alignment) noexcept {
std::cout << "Aligned delete\n";
std::free(ptr);
}
};
void* operator new(std::size_t size, void* location);
size 是对象地大小
location是一个指向预分配内存地指针。
用途
正确的实现
#include <iostream>
#include <cstdlib>
class MyClass {
public:
// Placement new
void* operator new(std::size_t size, void* location) {
std::cout << "Placement new called\n";
return location; // 返回指定的内存地址
}
// Placement delete
void operator delete(void* ptr, void* location) noexcept {
std::cout << "Placement delete called\n";
// 不需要释放内存,因为内存是由调用者管理的
}
() {
MyClassstd::cout << "Constructor called\n";
throw std::runtime_error("Error in constructor"); // 模拟异常
}
~MyClass() {
std::cout << "Destructor called\n";
}
};
int main() {
char buffer[sizeof(MyClass)]; // 预分配内存
try {
* obj = new (buffer) MyClass; // 使用 placement new
MyClass} catch (const std::exception& e) {
std::cout << "Exception caught: " << e.what() << "\n";
}
return 0;
}
// Placement new called
// Constructor called
// Placement delete called
// Exception caught: Error in constructor
解释:
如果没有定义对应的 placement delete,当构造函数抛出异常时:
成对定义 placement new 和 placement delete: 如果定义了 placement new,也必须定义对应的 placement delete。 如果定义了数组版本的 placement new[],也必须定义对应的 placement delete[]。
placement delete 的参数必须匹配 placement new: * 如果 placement new 接受额外的参数(如内存地址),placement delete 也必须接受相同的参数。
placement delete 不需要释放内存: * placement delete 通常不负责释放内存,因为内存是由调用者管理的。
示例:数组版本的placement new和delete
#include <iostream>
#include <cstdlib>
class MyClass {
public:
// Placement new[]
void* operator new[](std::size_t size, void* location) {
std::cout << "Placement new[] called\n";
return location;
}
// Placement delete[]
void operator delete[](void* ptr, void* location) noexcept {
std::cout << "Placement delete[] called\n";
}
};
int main() {
char buffer[sizeof(MyClass) * 3]; // 预分配内存
* arr = new (buffer) MyClass[3]; // 使用 placement new[]
MyClass// 注意:此处不会调用构造函数,因为 placement new[] 不会自动调用构造函数。
// 如果需要手动调用构造函数,可以使用 placement new 的单个版本。
return 0;
}