视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_cast
和static_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();
}
};