条款2
#define具有全局性,出来的是宏,它是预处理的东西,预处理后的编译阶段已经不存在,所以不可能获取宏的地址。
enum,非全局,内元素也不能取地址(右值),因为常量存放在内存里的位置没有"地址"这个概念,所以不能取地址
#include<iostream>
using namespace std;
void fun() {
#define AAA 222
};
class A{
#define BBB 333//不能用来定义class专属变量,不具有封装性
public:
#define CCC 444
};
int main() {
enum {aa=4,bb=1,cc=2,dd=3} b,c,d;
b = aa;
c = bb;
d = cc;
//cout<<&bb<<endl;//expression must be an lvalue or a function designator
cout<<typeid(b).name()<<endl;//Z4mainEUt_
cout<<&b<<endl;//0x7ffe363da5ac
cout<<b<<endl<<c<<endl<<d<<endl;//4 1 2
cout<<AAA<<endl;//222
cout<<BBB<<endl;//333
cout<<CCC<<endl;//444
}
小结:
涉及常量用const以及enum代替define, 函数宏则用inline代替define
条款3
#include<iostream>
using namespace std;
class A{
mutable char * text;
int length;
public:
A(char* p):text(p){length=1;};
char& operator[](int pos) const{
return text[pos];
}
void fun1(){}
void fun2()const{}
void print() const {
//fun1();//只能调用const成员函数
fun2();
//length = 1;//const成员函数不能修改成员变量
cout << this->text << endl;//读
text = "12312";//mutable修饰的变量可以在const函数中更改
}
void getText() const {
cout<<this->text<<endl;
}
};
int main() {
const A a("hello");
a.print();//hello
a.getText();//12312
}
条款4
构造函数初始化次序固定:先父类后子类,子类参数初始化按照声明的顺序
构造函数中,成员变量一定要通过初始化列表来初始化的几种情况
#include<iostream>
using namespace std;
class Base{
int b_a;
int b_b;
public:
Base(int b_a,int b_b):b_a(b_a),b_b(b_b){cout<<"base ctor"<<endl;}
//Base(){}
};
class Derived:public Base{
int d_a;
int d_b;
//int &c;//引用变量必须在构造函数通过初始化列表初始化
public:
Derived(int d_a,int d_b,int b_a,int b_b):d_a(d_a),d_b(d_b),Base(b_a,b_b)
{
cout<<"derived ctor"<<endl;
}
};
int main() {
Derived d(1,2,3,4);
// base ctor
//derived ctor
}
条款5
类成员有const变量或者引用,想要‘=’assignment操作必须要自己定义
拷贝引用会让两个变量指向一样的东西
const则不能被赋值
const int a = 1;//初始化
//a = 1;//不能被赋值
int a = 1;
int c = 4;
int &b=a;
b = c;//可以
int a = 1;
int c = 4;
int &b = a;
int &d = c;
b = d;//相同
cout << b << " "<< d <<endl;//4 4
int a = 1;
int c = 4;
const int &b=a;
//b = c;//不可
#include<iostream>
using namespace std;
class A{
int a;
const int b;
int &c;
public:
A(int a, const int b,int c):a(a),b(b),c(c){}
};
int main() {
A a(1,2,3);
A b(2,3,4);
//b=a;//出错
//main.cpp:10:7: error: non-static const member ‘const int A::b’, can’t use default assignment operator
//main.cpp:10:7: error: non-static reference member ‘int& A::c’, can’t use default assignment operator
}
条款7
虚析构函数:一般类里带有虚函数,就要有个虚析构函数。
如果没有虚函数,就没必要写虚析构了,会增加对象体积(多个vptr)
别去继承STL容器类如string,vector,list,set,unordered_map因为他们没有虚析构函数
什么是抽象类
声明了纯虚函数的类,都成为抽象类。抽象类只能作为基类来派生新类,不能声明抽象类的对象
详细:c++抽象类
虚析构和纯虚析构
#include<iostream>
#include<thread>
#include<vector>
#include<string>
#include<queue>
#include<mutex>
using namespace std;
class animal
{
public:
animal() { cout << "animal构造函数调用" << endl; }
//能调用子类析构函数 来释放堆区的解决方法:
//利用虚析构可以解决父类指针释放子类对象时不干净的问题
//virtual ~animal() { cout << "animal虚析构函数调用" << endl; }
//而如果想避免基类实例化,则可以将析构函数写成纯虚析构
//但因为派生类(子类)不可能来实现基类(父类)的析构函数,
//所以基类析构函数虽然可以标为纯虚,但是仍必须实现析构函数,
//否则派生类无法继承,也无法编译通过。
//纯虚析构 要声明也要实现(类外定义)
//有了纯虚析构之后,这个类也属于抽象类,并无法实例化对象
virtual ~animal() = 0;//纯虚析构
virtual void speak() = 0;//纯虚函数,必须在派生类中被实现
};
//纯虚析构函数必要类外定义
animal::~animal() { cout << "animal纯虚析构函数调用" << endl; }
//可写可不写
void animal:: speak() { cout << "父类纯虚函数" << endl; }
class cat : public animal
{
public:
cat(string name)
{
cout << "cat构造函数调用" << endl;
m_Name = new string(name);
}
virtual void speak() { cout << *m_Name << "小猫在说话" << endl; }
~cat()
{
if (m_Name != NULL)
{
cout << "cat析构函数调用" << endl;
delete m_Name;
m_Name = NULL;
}
}
string* m_Name;
};
void main()
{
//父类指针指向了一个开辟在堆区的子类数据
animal* a = new cat("Tom");
//new一个在堆区的cat类型的对象,cat继承自animal
//我们知道创建子类时会先创建父类
//所以先调用了animal的构造函数
//再是cat的构造函数
//亿点细节:
//在创建父类时,如果发现父类中有虚函数
//编译器就会使用一个叫虚函数表的东西,保存所有的虚函数,包括纯虚函数
//在这里,animal父类的虚函数表中,有两个虚函数
/*
class animal size(4):
+---
0 | (vfptr)
+---
animal::$vftable@:
| &animal_mata
| 0
0 | &animal::(dtor)
1 | &animal::speak
*/
//&animal::(dtor) 就是animal的纯虚析构函数的地址
//dtor是destructor的缩写,是析构函数的意思,中文直译叫"垃圾焚毁炉"
//&animal::speak 就是speak虚函数的地址
//接着,子类继承了这个表,重写了speak。
/*
class cat size(8):
+---
0 | +--- (base class animal)
0 | | {vfptr}
| +---
4 | m_Name
+---
cat::$vftable@:
| &cat_meta
| 0
| &cat::(dtor)
0 | &cat::speak
*/
//这个dtor我不知道算不算重写,我觉的应该是的
//但我不了解原理,所以搬了一个别人的解释:
//当父类析构函数定义为虚函数后,子类默认就是虚析构函数,跟普通成员函数一样
//现在将父类析构函数 定义为虚函数,子类再继承了 父类的虚函数表
//子类的虚函数表中,就存在父类的 虚析构函数的地址,
//但子类的析构函数 也是虚函数,所以重写了自己虚函数表中 父类虚析构函数的地址
//变成了子类的虚析构函数地址
//虽然父子的析构函数名字不一样,但是他们占同一个坑
//(即父子析构函数在虚函数表中的位置是一样的,否则就不存在多态了)
//用父类指针指向子类,并最后通过父类指针删除子类对象的时候,
//发现animal类的析构函数是虚函数,就到子类对象的虚函数表里找析构函数
//此时就会调用虚函表中 子类的虚析构函数 并很好的防止了内存泄漏
//然后还有个问题,
//既然子类的虚函数表中只有子类虚析构函数,那它怎样析构父类呢?
//父类的析构函数又在哪呢?
//其实在子类的析构函数中,底层包含着对父类析构函数的调用
//原因我认为是:
//对象都存放在栈中,创建子类对象先调用父类的对象,
//父类对象压入栈,然后是子类对象入栈,由于栈是先进后出,
//delete父类的时候就先调用子类对象,在调用父类对象
a->speak();
//因为子类的speak重写了父类的speak,父类指针调用的是子类的speak
delete a;
//父类指针在析构的时候 不会调用子类中的析构函数
//导致子类如果有堆区属性,出现内存泄漏
//原因是指针a是animal类型的指针,也就是父类类型的指针
//释放a时只进行animal类的析构函数。
//但父类析构函数变成虚析构之后,这个问题就解决了
}
/*
output:
animal构造函数调用
cat构造函数调用
Tom小猫在说话
cat析构函数调用
animal纯虚析构函数调用
*/
条款9
#include<iostream>
#include<vector>
using namespace std;
class Transaction//父类
{
public:
Transaction() { logTransaction(); }
virtual void logTransaction() const = 0;
};
//这里纯虚函数必须被定义,否则报错(因为要调用父类的虚函数)
void Transaction:: logTransaction() const
{
cout << "logTransaction log" << endl;
}
class BuyTransaction : public Transaction//子类1
{
public:
BuyTransaction() {}
virtual void logTransaction() const
{
cout << "BuyTransaction log" << endl;
}
};
class SellTransaction : public Transaction//子类2
{
public:
SellTransaction() {}
virtual void logTransaction() const
{
cout << "SellTransaction log" << endl;
}
};
int main() {
BuyTransaction b;//定义一个子类
//输出:logTransaction log
}
条款11
自我赋值不检查地址,会报错
delete p 不仅销毁当前对象的指针p,也销毁了要赋值对象的指针p,变成野指针
#include<iostream>
#include<vector>
using namespace std;
class A {
private:
int* p;
public:
A() { p = new int(1); }
A& operator=(const A& a) {
//if (this == &a)return *this;
delete p;
p = NULL;//delete指针后,一定要将其置空(原书没有这句,但是调试发现delete后p仍存在(空间仍在),只是指向的区域的值变得不确定)
p = new int(*a.p);//成员函数内允许实例对象访问私有变量
return *this;
}
};
int main() {
A a;
a = a;//报错
}
条款12
当有类继承时,要完整实现拷贝构造和拷贝赋值。可以在子类拷贝构造中调用父类的拷贝构造(不能调用父类的拷贝赋值),子类拷贝赋值中调用父类的拷贝赋值(不能调用父类的拷贝构造)
条款15
share_ptr提供一个get成员函数,用来执行显示转换,也就是它会返回智能指针内的原始指针的复件
std::tr1::shared_ptr<Investment>pInv(createInvsetment());//智能指针
int fun(const Investment* pi);
int d = fun(pInv);//错误!类型不一样
int d = fun(pInv.get());//正确!
显示转换安全,但是每次都要调用get
class FontHandle{};
class Font {
FontHandle f;
public:
explicit Font(FontHandle f):f(f) {}
~Font() {};
FontHandle get()const { return f; }
};
隐式转换方便,但是不安全
class FontHandle{};
class Font {
FontHandle f;
public:
explicit Font(FontHandle f):f(f) {}
~Font() {};
operator FontHandle() { return f; }//隐式转换
};
隐式转换的例子
#include <iostream>
class Complex {
private:
double real_;
double imag_;
public:
Complex(double real, double imag)
: real_(real)
, imag_(imag) {}
// 转换构造函数
Complex(double real)
: real_(real)
, imag_(0) {}
// 隐式转换函数
operator double() { return real_; }
friend std::ostream& operator<<(std::ostream&, const Complex&);
};
std::ostream&
operator<<(std::ostream& os, const Complex& cp) {
if (cp.real_) {
os << cp.real_;
}
if (cp.imag_) {
if (cp.real_) {
os << '+';
}
os << cp.imag_ << 'i';
}
return os;
}
int
main(void) {
Complex c1(1.2, 3.5);
Complex c2 = 4.4; // 对 4.4 调用了转换构造函数
double d = 1.8 + c1; // 对 c1 调用了类型转换函数
std::cout << "c1 = " << c1 << std::endl;
std::cout << "c2 = " << c2 << std::endl;
std::cout << "d = " << d << std::endl;
return 0;
}
条款17
考虑函数
void processWidget(std::tr1::shared_ptr <Widget>,int priority );
实现:
processWidget(std::tr1::shared_ptr<Widget>(new Widget),priority());
编译器可能的执行顺序:
1.执行new Widget
2.调用priority
3.调用tr1::shared_ptr构造函数
万一在执行第二步时抛出异常,此时new Widget返回的指针将会遗失,因为它未被置入tr1::shared_ptr内,引发资源泄漏!
解决办法:独立语句执行
std::tr1::shared_ptr<Widget>pw(new Widget);
processWidget(pw,priority());
条款18
条款20
class A {//父类
public:
string name()const;
virtual void display()const;
};
class B :public A {//子类
public:
virtual void display()const;
};
//函数
void func(A a) {
cout << a.name() << endl;
a.display();
}
B b;
func(b);//pass-by-value,子类对象会被切割成父类,调用A::name()和A::display()绝对不会是B::name()和B::display()
正确的方法是pass by reference-to-const
void func(const A& a) {//实现多态!
cout << a.name() << endl;
a.display();
}
总结:
1.pass by reference通常意味着真正传递的是指针。因此如果有个对象属于内置类型(如int)或者STL的迭代器和函数对象,通常还是pass by value效率高
2.其他情况还是pass-by-reference-to-const好,如上代码还可以避免切割问题