Effective C++ 基础部分

视C++为一个语言联邦

C++是一个多重泛型编程语言,我们可以认为其由4个子语言组成:

  • C: C++刚开始时是C with class
  • Object-Oriented C++: C++面向对象的特性部分。如封装、继承、多态
  • Template C++: 泛型编模板元编程
  • STL:算法、容器、迭代器、函数对象等

用const、enum、inline替代#define

  • 对于常量,应该以const对象或者enum来代替#define
  • 对于宏函数,最好使用inline来替代#define

const和enum

#define是预处理,并不被当作语言的一部分,而是直接做文本替换,可能会带来意料外的错误。
#define printf cout会改变函数定义

形如

#define PI = 3.1415926
const double Pi = 3.1415926;

Pi会进入编译器的符号表,从而方便后续操作。

常量指针
定义常量指针,一般都是放在头文件的,因此最好声明为

const char* const name = "Tom";
// 更适用于string
const std::string name("Tom");

类中静态常量

struct A{
	static const int Number = 0;
	// 使用该常量
	int scores[Number];
};

一般编译器都支持默认类型可以在类内声明时赋值,只要不对其进行取地址操作,就可以直接使用. 否则的话需要单独定义.

struct A{
	static const int Number = 0;
	// 使用该常量
	int scores[Number];
};

/// in xx.cc文件
// 声明时赋过初值了.不可以再次赋值,否则会报错
const int A::Number;

enum hack
可以使用enum类型来定义常量,但是其特点是:

  • 不可以取地址. 所以通过enum hack,不可以获得指向某个整数常量的指针.

用来解决某些编译器不允许类内初始化的问题

struct A{
	enum {Number = 6;}
	int score[Number];
};

define与inline

一个例子.define可能会造成意外结果

#define MAX(a, b) (a) > (b) ? (a) : (b);

template<typename T>
inline T findMax(const T &a, const T &b) {
    return a > b ? a : b;
}
int main() {
    int a = 1;
    int b = 1;
    int c = MAX(++a, b);
    cout << a << " " << b << " " << c << endl;
    a = 1, b = 1;
    c = findMax(++a, b);
    cout << a << " " << b << " " << c << endl;
}
/*
3 1 3
2 1 2
*/

使用宏函数的例子中,++a被执行了两次

尽可能使用const

  • 将某些需要的类型声明为const可以帮助编译器检查错误用法。 const可以修饰作用域内的任意成员函数、对象类型、函数参数、返回类型。
  • 编译器强制实行bitwise constness(不能在const函数内改变任何对象的指),但是不保证logical constness(可以修改指针指向的非对象内的指)。
  • 当const重载的函数内容一致时,可以通过非const函数调用const版来精简代码。

const与stl迭代器

迭代器是以指针为根据塑造出来的,其作用类似于一个T*的指针,因此直接作用于stl迭代器的const,类似于一个const指针,可以改变值但是不能改变指向. 想要一个指向const的迭代器,应该是const_iterator

int main() {
    vector<int> vec{1, 2, 3, 4};

    const auto it1 = vec.begin();
    (*it1) = 0;
//    it1++;
    vector<int>::const_iterator it = vec.begin();
//    (*it) = 1;
    it++;
}

函数返回const–减少意外赋值错误

可以减少if(a + b = c)这样将==意外打成=的错误
其中a b是重载的类型

const Type operator*(const Type& a, const Type& b){
	// ...
	return a;
}

这样就无法对其返回类型赋值,减少意外操作.

const成员函数

  • 使成员函数可以作用于const对象
  • 使接口便于理解

const可以重载成员函数

struct A {
    string s;
	
    A(string v) : s(v) {}

	// 返回类型必须是引用,因为内置返回类型无法被修改
    char &operator[](int i) {
        return s[i];
    }

    const char &operator[](int i) const {
        return s[i];
    }
};

int main() {
    A a("hello");
    a[0] = 'H'; // 调用非const函数
    cout << a.s << endl;

    const A b("hello"); // 调用const函数
    cout << b[0];
}

指针绕过const
const成员函数不可以修改对象内的任何non-static内容,但是如果const函数内,我们只修改了一个指针指向的值,而只有指针属于这个对象,其所指内容不属于对象,那么就可以通过const检查.但是不符合bitwise constness概念.如:

struct A {
    char *s;
	static int a;
    A(char *v) : s(v) {}

    void func() const {
        s[1] = 'L';
        a++; // 合法,是静态成员
    }
};
int A::a = 0;
int main() {
    char *c = new char[10];
    memset(c, '\0', 10);
    strcpy(c, "AAAA");
    A a(c);
    a.func();
    cout << a.s;
    // ALAA
}

mutable关键字可以使non-static对象改变

struct A {
    char *s;
    mutable int len;
    int getL() const{
        len = strlen(s);
        return len;
    }
};

在const和非const之间避免重复代码

利用const函数构造其非const的孪生版本
如上文,通过const可以重载函数,有时候两个函数之间代码完全一致,但是通过const重载会造成代码冗余,进而导致编译时间、维护难度等上升。

struct A {
    string s;
	
    A(string v) : s(v) {}

	// 返回类型必须是引用,因为内置返回类型无法被修改
    char &operator[](int i) {
    	// 可能的边界检查代码
    	// 可能的数据访问代码
    	// 可能的数据完整性校验
        return s[i];
    }

    const char &operator[](int i) const {
    	// 可能的边界检查代码
    	// 可能的数据访问代码
    	// 可能的数据完整性校验
        return s[i];
    }
};

我们可以通过const_caststatic_cast来完成代码复用。

struct A {
    string text;

    A(string s) : text(s) {}

    const char &operator[](int i) const {
        // 可能的边界检查代码
    	// 可能的数据访问代码
    	// 可能的数据完整性校验
        return text[i];
    }

    char &operator[](int i) {
        return const_cast<char &>(static_cast<const A &>(*this)[i]);
    }

};

如上,我们通过static_cast<const A&>(*this)将原类型转化为const类型,然后调用[],就会访问const函数。但是其返回值是const char&, 所以我们通过const_cast将其移除const属性。

注意:在非const函数内调用const函数是安全的,在const函数内调用非const函数是不安全的!

确定对象被使用前已经被初始化

  • C++并不保证初始化成员对象,所以对内置对象进行手动初始化
  • 构造函数最好使用列表初始化,列表初始化的初始化顺序与成员对象被声明的顺序相关,与在列表中出现的位置无关
  • 为了避免跨编译单元间的初始化问顺序问题,使用局部静态变量代替非局部静态变量

手动初始化成员对象

在类内部,并不保证初始化的值。

struct C{
    int x, y;
    char c;
};

int main() {
    C c;
    cout<<c.x<<" "<<c.y<<" -"<<c.c;
}
// 32759 0 -

这个例子里,结构体内部没有像我们想象的一样,默认初始化成员变量为0.

使用列表初始化

使用列表初始化一个成员对象:直接调用对应对象的拷贝构造函数
在函数体内初始化:先对该对象调用默认初始化函数,然后调用拷贝赋值运算符,会造成性能损失.
例子,这是基类:

struct Base {
    Base() {
        printf("Base默认构造函数\n");
    }

    Base(const Base &) {
        printf("Base拷贝构造函数\n");
    }

    Base& operator=(const Base& c){
        printf("Base拷贝赋值运算符\n");
    }


    ~Base() {
        // printf("Base析构了\n");
    }
};

如果使用函数体内初始化:

struct B {
    Base a;
    
    B(Base &c) {
        a = c;
    }

    B() {}
};


int main() {
    Base a;
    B b(a);
}
/*
Base 默认构造函数
// 下面是b的构造过程:
Base 默认构造函数
Base 拷贝赋值运算符
*/

使用列表初始化

struct B {
    Base a;
	// 记得声明为引用类型
    B(Base &c) : a(c) {}

    B() {}
};


int main() {
    Base a;
    B b(a);
}
/*
Base默认构造函数
Base拷贝构造函数
*/

这样减少了一次默认构造过程,当成员对象复杂时,会减少很多操作.

尽量对所有成员对象使用列表初始化,但是对于内置类型(int、char等,赋值和初始化性能表现一样),可以定义init()函数并在构造函数内复用,减少代码复杂度.

要注意的时,在声明拷贝构造函数时,必须将参数设置为引用(不然会无穷递归调用拷贝构造函数,编译器会直接报错)

在声明一个类的构造函数,参数里的成员对象一般声明为引用类型,不然会造成一次拷贝构造操作,造成浪费

struct A {
    A() {}

    A(const A &) {
        printf("构造了A\n");
    }
};

struct C {
    A a;
	// 没有声明为引用
    C(A _a) : a(_a) {}
};

int main() {
    A a;
    C c(a);
}
/*
构造了A
构造了A
*/

这是因为直接传参,而不是引用,那么会在栈上构造一个对应的副本,这个时候就会调用该对象类型的拷贝构造函数,然后初始化内部对象的时候又调用了一次. 所以记得参数一般要声明为const Type&

类成员对象以其被声明顺序完成初始化

struct A {
    A() {}

    A(const A &) {
        printf("构造了A\n");
    }
};

struct B {
    B() {}

    B(const B &) {
        printf("构造了B\n");
    }
};

struct C {
	// 先定义了B,在定义了A
    B b;
    A a;
    C(A &_a, B &_b) : a(_a), b(_b) {}
};


int main() {
    A a;
    B b;
    C c(a, b);
}
/*
构造了B
构造了A
*/

为了避免混淆,尽量在列表初始化时,出现顺序和声明顺序一致

不同编译单元内定义的non-local static对象初始化顺序

struct Base{
	size_t getSize() const;
};

extern Base base;

/// 其他用户
struct Client{
	void func(){
		base.getSize();
	}
};

这就有可能出现Client调用base的时候,base还没初始化.

解决办法:单例模式

struct Base{
	size_t getSize() const;
	
	Base& getInstance(){
		static Base base;
		return base;
	}
};

struct Client{
	void func(){
		Base::getInstance().getSize();
	}
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值