0%

C++基础

这篇文章主要讲述C++语言的基础。文章不涉及C++最基础的语法,主要为一些熟悉C++后需要注意和学习的点。包括的内容有const,mutable,virtual等。

const修饰

  • const成员函数不能调用非const成员函数,不能改变非static成员变量,可以改变static成员变量。
  • const成员函数可以改变mutable关键字修饰的成员变量。
  • const实例不能调用非const成员函数。
  • const type* pobj表示pobj是指向的内容不可变;type* const pobj = &instance表示pobj不能改,而且需要初始化。
  • 返回值设置为const可以避免像(a * b) = c这样的错误。当a和b为内置类型时,那直接了当的不合法。但当a和b为自定义类型时,是有可能是合法的,当为了避免这种没有意义的语句,将operator*的返回值设为const。
  • const_iterator迭代器指向的值不可更改;const std::vector<int>::iterator iter = vec.begin()指iter不可更改。
  • 类内const int x = 100;在某些c++版本中不合法,建议用enum代替实现。
1
2
3
4
5
6
7
8
9
10
11
12
/**
* 类内使用const成员变量必须要在构造函数中初始化。
**/
class Base {
public:
const int x;
Base():x(1) {}

int& print(int) {
std::cout<<"in const function" <<std::endl;
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 用const版本函数实现non-const版本函数
**/
#include <iostream>

class Base {
public:
Base() {}

const int& print(int) const {
std::cout<<"in const function" <<std::endl;
}

int& print(int) {
std::cout<<"in non-const function" <<std::endl;
return const_cast<int&> ((static_cast<const Base>(*this)).print(1));
}
};

int main(void) {
Base obj1;
const Base obj2;
obj1.print(1);
obj2.print(2);

return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>

class Base {
public:
Base() {}

const int& print(int) const {
std::cout<<"in const function" <<std::endl;
}

int& print(int) {
std::cout<<"in non-const function" <<std::endl;
}
};

int main(void) {
Base obj1;
const Base obj2;
obj1.print(1); // print: in non-const function
obj2.print(2); // print: in const function

return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
class Base {
public:
Base() {}
int& print(int) {
std::cout<<"in non-const function" <<std::endl;
}
};

int main(void) {
Base obj1;
const Base obj2;
obj1.print(1); // ok
obj2.print(2); // 错误,const对象不能调用非const函数

return 0;
}

mutable修饰

mutable关键字意思是可变的,是与const相对的关键字。
在类中,const成员函数不能修改非静态的成员变量。但是,使用mutable可以突破限制,const函数可以修改mutable修饰的非静态成员变量。

mutable只能用于修饰非静态成员变量。

explicit修饰

explicit关键字只能用于修饰类的构造函数,包括拷贝构造函数。表明该构造函数是显式的,即是不允许隐式转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
#include <string>

void log(const std::string& msg) {
std::cout<<msg <<std::endl;
}

class Base {
public:
explicit Base(int){
log("in base construct func...");
}

explicit Base(const Base& in) {
log("in base copy contruct func...");
}
};

class Derive: public Base {
public:
Derive():Base(1) {}
};

int main(void) {
Base b = 1; // 编译不通过。 如果Base(int)没有explicit修饰则不会有编译错误。
// 编译器需要转换为 Base temp(1); Base b = temp;
// 但是编译器只会调用构造函数,不会调用拷贝构造
Derive d;
Base b2 = d; // 编译不通过 如果Base(const Base& in)没有explicit修饰则不会有编译错误

return 0;
}

extern修饰

extern修饰一个变量时,表示这个变量为全局变量。

1
2
3
4
5
6
7
8
/**
* main2.cpp
*/
int global = 1132; // 可以跨文件使用,其他文件不能定义名为global的变量
int &globalr = global; // 可以跨文件使用,其他文件不能定义名为globalr的变量
static int local1 = 12; // 只能在本文件内使用,其他文件可以在定义名为local1的变量
const int local2 = 13; // 只能在本文件内使用,其他文件可以在定义名为local2的变量
extern const int hello = 14; // 可以跨文件使用,其他文件不能定义名为hello的变量
1
2
3
4
5
6
7
8
9
10
11
12
/**
* main.cpp
**/
int main() {
extern int global;
std::cout<<global; // 输出1132
extern const int local2; //声明extern const并不会与main2.cpp中的local2冲突 [static情况同const]
std::cout<<local2; // 编译错误,undefined reference to local2 [static情况同const]
extern const int hello;
std::cout<<hello; // 输出14
return 0;
}

因此,不能在头文件中定义全局变量。

volatile修饰

volatile意思为「不稳定的」。修饰变量时,表示变量可能会被未知的因素修改,比如硬件等。对于使用volatile修饰的变量,每次读取数据,系统都从变量所在内存重新读取。

virtual修饰

虚函数

虚函数实现多态性。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Base {
public:
virtual void print() {
log("in Base...");
}
};
class Derive: public Base {
public:
virtual void print() { // 此时,没有virtual关键字,print函数也是虚函数
log("in Derive...");
}
};
int main(void) {
Derive *d = new Derive();
Base *b = d;
b->print(); // 输出in Derive...
b->Base::print(); // 强制使用基类的print函数
return 0;
}

注意

  • 只有在通过基类指针或引用间接指向派生类子类型时多态性才会起作用!
  • 子类中的虚函数的声明必须与基类中的定义方式完全匹配。但有一个例外:返回对基类的引用(或指针)的虚函数,子类中的虚函数可以返回基类函数所返回类型的子类的的引用(或指针)。
  • 通过基类指针或引用调用虚函数时,实参的默认值为在基类虚函数声明中指定的值。如果通过派生类的指针或引用调用虚函数,则默认实参是在派生类的版本中声明的值。

对于父类和子类相同名字函数,

  • virtual修饰
    基类函数:virtual Base::print
    子类函数:virtual Derive:print
    • 两个函数print具有相同的返回值类型(对于引用和指针有例外)和相同的形参列表:basePtr->print调用子类print,体现多态性
    • 两个函数print具有不同形参列表:子类中的Base::print被隐藏,basePtr->print调用基类print
    • 两个函数print具有不同的返回值,相同的形参列表:编译错误,overriding error…
  • 无virtual修饰
    基类函数:Base::print
    子类函数:Derive:print
    • 基类中的Base::print被隐藏

被隐藏的基类函数,可以通过Base::print的方式调用。 pderive->Base::print();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Base {
public:
Base() {}
void print() {
log("in Base...");
}

void anotherPrint(){
log("in Base...anotherPrint");
}
};
class Derive: public Base {
public:
void print(int) {
log("in Derive...");
}
};
int main(void) {
Derive *d = new Derive();
d->print(1); // 调用子类print,输出in Derive...
d->anotherPrint(); // 调用基类anotherPrint,输出in Base...anotherPrint
d->print(); // error: no matching function for call to 'Derive::print()'
return 0;
}

虚函数底层实现

参考陈皓的博客C++虚函数表解析

纯虚函数

纯虚函数:在函数声明后添加=0,将函数声明为纯虚函数。
含有一个或多个纯虚函数的类是抽象基类。不能创建抽象基类实例。
纯虚函数前有virtual后有=0修饰,缺一不可。

1
2
3
4
class Abstract {
public:
virtual void printHello() = 0;
};

抽象基类中的纯虚函数可以实现,也可以不实现。

虚析构

应为多态基类声明virtual析构函数。详细见《Effective C++》条款07。
当delete掉一个基类指针时,如果这个基类指向的对象是子类对象:

  • 子类含有虚析构,那么会先调用子类的虚构函数,再调用基类的虚构函数
  • 子类不含有虚析构,那么只会调用基类的虚构函数。而没有调用子类的析构函数,很有可能会造成内存泄露。

对比有virtual虚构和无virtual虚构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 无virtual虚构
**/
class Base {
public:
~Base() {
log("in Base dst...");
}
};
class Derive: public Base {
public:
~Derive() {
log("in Derive dst...");
}
};
int main(void) {
Derive *pderive = new Derive();
Base *pbase = pderive;
delete pbase; // 输出:in Base dst...
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 有virtual析构
**/
class Base {
public:
virtual ~Base() {
log("in Base dst...");
}
};
class Derive: public Base {
public:
virtual ~Derive() {
log("in Derive dst...");
}
};
int main(void) {
Derive *pderive = new Derive();
Base *pbase = pderive;
delete pbase; // 输出:in Derive dst... (new line) in Base dst...
return 0;
}

虚继承

先上代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Base {
public:
Base(int v): var(v) {log("Base");}
int var;
};
class Left: virtual public Base {
public:
Left(int v): Base(v) {log("Left");}
};
class Right: virtual public Base {
public:
Right(int v): Base(v) {log("Right");}
};
class Derive:public Left, public Right {
public:
Derive():Left(1), Right(2), Base(3) {log("Derive");} // Base的初始化由Derive负责
};
int main(void) {
Derive d; //输出:Base Left Right
std::cout<<d.var; //输出3
return 0;
}

Derive d实例中,只有一个Base子对象。
如果Left和Right不是virtual public继承自Base,那么一个Derive对象中可能会存在两个Base子对象,并且在Derive对象的构造过程中Base构造函数将被调用两次。

Left和Right虚继承自Base,因此Base被称为虚基类。
虚基类的初始化由最底层的子类负责。在此例中是Derive负责。

字符串

C字符串和C++string的size

由于c字符串以\0结尾,同样的字符,c字符串和string字符串的长度并不一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <cstring>
#include <string>

using namespace std;

int main() {
char ch[] = "Hello";
cout<<sizeof(ch) <<endl; // 6
cout<<sizeof("Hello") <<endl; // 6

string s= "Hello";
cout<<s.length() <<endl; // 5
cout<<s.size() <<endl; // 5
}

使用strcpy等函数的隐患

1
char* strcpy(char *destination, char *source);  // 返回的char*为char *destination

隐患有两点。

  • source字符串中没有’\0’结束符
  • destination字符串没有足够的空间存储source字符串

改进以避免隐患

  • 使用strncpy等函数,并保证destination有足够的空间
  • 使用微软的CRT函数
    1
    errno_t strcpy_s(char* dest, size_t numElems, const char* src, size_t count);

typedef

1
typedef void (CCObject::*SEL_SCHEDULE)(ccTime);

表示SEL_SCHEDULE为一个 参数类型为ccTime,返回类型为void的函数指针类型。

构造&拷贝构造&赋值操作符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <iostream>
#include <string>

void log(const std::string& msg) {
std::cout<<msg <<std::endl;
}

class Base {
public:
Base(){
log("in base construct func...");
}

Base(int) {
log("in base construct func...");
}

Base(const Base& in) {
log("in base copy contruct func...");
}

Base& operator=(const Base&) {
log("in base operator= func...");
return *this;
}
};

class Derive: public Base {

};

void func(Base) {}

int main(void) {
Base b1; // contruct
Base b2 = b1; // copy contruct
b2 = b1; // operator=

Derive d; // contruct
func(b1); // copy contruct
func(d); // copy contruct
func(1); // contruct

return 0;
}

函数重载

出现在相同作用域内的两个函数,如果具有相同的名字而形参表不同,则称为函数重载。
重载的函数之间不能根据返回值类型来区分,只能根据形参表来区分。

具有相同函数名的两个函数的函数声明:

  • 如果两个函数的返回值类型和形参列表完全相同,则第二个函数声明为第一个函数的重复声明;
  • 如果两个函数的形参列表完全相同,但返回值类型不同,则第二个声明是错误的。

函数重载时选择函数的策略:根据实参。寻找最佳匹配时,实参转换等级以降序排列如下:

  1. 精确匹配
  2. 通过类型提升实现的匹配
  3. 通过标准转换实现的匹配
  4. 通过类类类型实现的匹配

那么,当出现不同的实参匹配到不同的重载函数时,则会编译错误。

例如:

1
2
3
4
5
6
void func(int, int);
void func(double, double);

int i = 1;
double d = 1.0;
func(i, d);

显然,根据第一个实参i进行匹配,void func(int, int)是最佳匹配。根据第二个实参d进行匹配,最佳匹配时void func(double,double)。 此时,会出现编译错误(ambiguous)。

操作符重载

不能重载的操作符有:「::」,「.」,「*」,「?:」,「sizeof」

  • 重载操作符必须具有一个类类型操作数。用于内置类型的操作符,其含义不能改变。
  • 不能创造新符号。
  • 优先级和结合性是固定的。
  • 不具备短路求值特性。重载操作符不保证操作数的求值顺序。并且,在&&和||的重载中,两个操作数都要进行求值,而且求值顺序不作规定。

重载一元操作符,作为成员函数时没有显示形参,作为非成员函数时就只有一个形参。
重载二元操作符,作为成员函数时有一个形参,作为非成员函数时有两个形参。

作为成员函数的操作符,有一个隐含的this形参,限定为第一个操作数。
当操作符重载定义为非成员函数时,通常必须将它们设置为所操作类的友元。

因此,对于<<和>>操作符重载,需要使用友元:

1
2
3
4
5
6
7
8
9
10
class Base {
public:
friend std::ostream& operator<<(std::ostream& out, const Base &b);
};

std::ostream& operator<<(std::ostream& out, const Base &b)
{
out<<"Base<<" <<std::endl;
return out;
}

类型转换

The difference is that in C++ you have various types of casts:

  • static_cast which is for “safe” conversions;
  • reinterpret_cast which is for “unsafe” conversions;
  • const_cast which is for removing a const attribute;
  • dynamic_cast which is for downcasting (casting a pointer/reference from a superclass to a subclass).

隐式转换

情形1:基本数据类型赋值转换

1
2
3
short a = 128;
int b;
b = a;

情形2:可被单参调用(只有一个参数或多个参数但至少从第二个参数起均带有缺省值)的构造函数或隐式类型转换操作符也会引起隐式类型转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void log(const std::string& msg) {
std::cout<<msg <<std::endl;
}

class Base {
public:
Base(){
log("in base construct func...");
}
};

class Other {
public:
Other() {}
Other(Base b) {}
};


int main(void) {
Base b;
Other o;
o = b;

return 0;
}

在C++中,总共有5中显示类型转换方法。

C风格转换

1
2
3
double d;
int i;
i = (int)d;

static_cast (var)

static_cast 很像 C 语言中的旧式类型转换。

  • 它能进行基础类型之间的转换
  • 也能将带有可被单参调用的构造函数或用户自定义类型转换操作符的类型转换
  • 还能在存有继承关系的类指针之间进行转换(即可将基类指针转换为子类指针,也可将子类指针转换为基类指针)。对于有继承关系的实例,基类转换为子类需要子类有能够接受基类参数的构造函数。
  • 还能将 non-const对象指针转换为 const对象指针(注意:反之则不行,那是const_cast的职责。)

注意,以下转换是错误的:

1
2
int *pint = NULL;
char *pchar = static_cast<char*>(pint);

Reason:
int needs more memory than what char occupies and the conversion cannot be done in a safe manner.
If you still want to acheive this,You can use reinterpret_cast, It allows you to typecast two completely different data types, but it is not safe.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
#include <iostream>
#include <string>

void log(const std::string& msg) {
std::cout<<msg <<std::endl;
}

class Base {
public:
Base(){
log("in base construct func...");
}

Base(int) {
log("in base construct func...");
}

Base(const Base& in) {
log("in base copy contruct func...");
}

Base& operator=(const Base&) {
log("in base operator= func...");
return *this;
}

~Base() {
// log("in base destruct...");
}
};

class Derive: public Base {
public:
Derive(){
log("in Derive construct func...");
}

Derive(Base&) {
log("in Derive construct func...");
}

Derive(const Derive& in) {
log("in Derive copy contruct func...");
}

Derive& operator=(const Derive&) {
log("in Derive operator= func...");
return *this;
}

~Derive() {
// log("in Derive destruct...");
}
};

class Other {
public:
Other() {
log("in other construct func...");
}

Other(Base&) {
log("in other construct func with Base...");
}

Other(const Other&) {
log("in other copy construct func...");
}

Other& operator=(const Other&) {
log("in other operator= func...");
return *this;
}

};


int main(void) {
double x = 0.01;
int i = static_cast<int>(x);

// 非const转为const,但不能将const转成const
const int j = static_cast<const int>(i);
int k = static_cast<int>(j); // 可通过编译
const int *pi = &i;
// int *pj = static_cast<int*>(pi); //编译失败

Base b;
Other o;
o = static_cast<Other>(b); // Other temp(b); o = temp;
// 调用顺序为 Other cst with Base, Other=

Derive d, d1;
d = static_cast<Derive>(b); // 基类转子类,需要子类有能够接受Base的构造函数。与将Base转成Other情况相同
// 调用顺序:base cst, derive cst, derive= (掉用前两个函数建立Derive实例)
b = static_cast<Base>(d1); //子类转基类,调用顺序:Base copy cst,base=

Base *pb = new Base();
Derive *pd1 = new Derive(), *pd2;
pd2 = static_cast<Derive*>(pb); // 基类指针转子类指针,不进行运行时检查,不安全。dynamic_cast可以安全转型
pb = static_cast<Base*>(pd2); // 子类指针转基类指针

return 0;
}

dynamic_cast (var)

dynamic_cast主要用来在继承体系中的安全向下转型。

向下转型

它能安全地将指向基类的指针转型为指向子类的指针或引用,并获知转型动作成功是否。如果转型失败会返回null(转型对象为指针时)或抛出异常(转型对象为引用时)。dynamic_cast 会动用运行时信息(RTTI)来进行类型安全检查,因此dynamic_cast存在一定的效率损失。

使用dynamic_cast<type> (var)的条件为var(需要被转型的变量)必须是多态类(有虚函数)或者是子类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
#include <string>

void log(const std::string& msg) {
std::cout<<msg <<std::endl;
}

class Base {
public:
virtual void func() {}
};

class Derive: public Base {
public:
virtual void func() {}
};

class Other {
virtual void func() {}
};

int main(void) {
Base *pb1 = new Base();
Derive *pd1 = dynamic_cast<Derive*> (pb1); // NULL

Derive *pd2 = new Derive();
Base *pb2 = dynamic_cast<Base*> (pd2);

Base *pb3 = pd2;
Derive *pd3 = dynamic_cast<Derive*> (pb3);

Other *po = new Other();
Derive *pd4 = dynamic_cast<Derive*>(po); // NULL

return 0;
}

横向转型

向上转型是多态的基础,需不要借助任何特殊的方法,只需用将子类的指针或引用赋给基类的指针或引用即可,当然dynamic_cast也支持向上转型,而其总是肯定成功的。而对于向下转型和横向转型来讲,其实对于dynamic_cast并没有任何区别,它们都属于能力查询。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Shape {
public: virtual ~Shape();
virtual void draw() const = 0;
};

class Rollable {
public: virtual ~Rollable();
virtual void roll() = 0;
};

class Circle : public Shape, public Rollable {
void draw() const;
void roll();
};

class Square : public Shape {
void draw() const;
};


//横向转型失败
Shape *pShape1 = new Square();
Rollable *pRollable1 = dynamic_cast<Rollable*>(pShape2); // pRollable为NULL

//横向转型成功
Shape *pShape2 = new Circle();
Rollable *pRollable2 = dynamic_cast<Rollable*>(pShape2); // pRollable不为NULL

const_cast

const_cast可去除对象的常量性(const),它还可以去除对象的易变性(volatile)。const_cast的唯一职责就在于此,若将const_cast 用于其他转型将会报错。

reinterpret_cast

reinterpret_cast用来执行低级转型,如将执行一个int的指针强转为 int。其转换结果与编译平台息息相关,不具有可移植性。
reinterpret_cast 常用的一个用途是转换函数指针类型,即可以将一种类型的函数指针转换为另一种类型的函数指针,但这种转换可能会导致不正确的结果。

signed和unsigned

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <iostream>

using namespace std;
int main() {
unsigned int ui;
unsigned char uc;
signed int si;
signed char sc;
/*****************************************************
* signed int和unsigned int, signed char和unsigned char
* 直接按位拷贝
*****************************************************/
si = -1; // (1111 1111 1111 1111 1111 1111 1111 1111)
ui = si; // ui = 4294967264
// 即(1111 1111 1111 1111 1111 1111 1111 1111)
sc = -1; // (1111 1111)
uc = sc; // uc = 255, 即(1111 1111)
/*****************************************************
* int转换为char,不管signed还是unsigned,
* 直接截取低8位
*****************************************************/
/* unsigned int 转换为 unsinged char */
ui = 0xffe0;
uc = ui; // uc = 224
/* unsigned int 转换为 signed char */
sc = ui; // sc = -32
/* signed int 转换为 signed char */
si = -129; // (11111111 11111111 11111111 0111 1111)
sc = si; // sc = 127 = (0111 1111)
/* signed int 转换为 unsigned char */
uc = si; // uc = 127
/*****************************************************
* char转换为int
* 当char为signed char,且值为负数时,int高位补1
* 补1后再看int是signed还是unsigned来求职
*****************************************************/
/* unsigned char 转换为 unsigned int */
uc = -1; // 此时, uc为255(1111 1111),相当于:
// signed char temp = -1;
// uc = temp;
ui = uc; // ui = 255
/* unsigned char 转换为 signed int */
uc = 0xe0; // 224(1110 0000)
si = uc; // si = 224, 拷贝char的低8位到int低8位
/* signed char 转换为 unsigned int */
sc = 0xe0; // sc = -32(1110 0000)
ui = sc; // ui = = 4294967264,即(1111 1111 1111 1111 1111 1111 1110 0000)2
/* signed char 转换为 signed int */
sc = 0xe0; // sc = -32(1110 0000)
si = sc; // si = -32(1111 1111 1111 1111 1111 1111 1110 0000)2
}

内存类型

程序占用的内存主要分为:

  1. 栈区(stack)—由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
  2. 堆区(heap)—一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。C++中的new和malloc申请的内存都属于这一块。
  3. 全局区(静态区)(static),未初始化的全局变量和静态变量的存储是放在一块的,初始化的全局变量,静态变量和const变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 程序结束后由系统释放。
  4. 文字常量区—常量字符串就是放在这里的。程序结束后由系统释放
  5. 程序代码区—存放函数体的二进制代码。

栈是由高地址向低地址扩展的数据结构,是一块连续的内存的区域。但是分配在栈中的数组,下标小的元素处于低地址。

区分以下两种情况:

1
2
char *pchar = "Hello World";  // pchar分配在栈中,而"Hello World"存储在常量区,因此pchar可以作为函数返回值。
char charArray[] = "Hello World"; // charArray是一个存储着"Hello World"的数组,内存分配在栈中。

内存管理

new和delete

  • new operator & delete operator
    new操作符和delete操作符像sizeof等操作符一样,语言内置,不能进行改变。
    new操作符做的工作:调用操作符new(new函数,与new操作符区别)分配内存,调用对象的构造函数等进行初始化。
    delete操作符的工作:调用对象析构函数,操作符delete释放内存。

  • operator new & operator delete
    operator new和operator delete类同C语言中的malloc和free函数,用于申请内存和释放内存。并不会调用析构函数。
    operator new函数,一般这样子声明:

    1
    void* operator new(size_t size);

operator new和operator delete是允许程序员根据自己的需要去重新实现的,而new operator和delete operator则不可以。

  • new[] & delete[]
    new[]和delete[]用于申请数组和释放数组。delete[]只是去释放数组本身的内存。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;

class Base {
public:
~Base() { cout<<"in Base dst" <<endl; }
};

int main() {
Base *baseArray = new Base[2];
delete[] baseArray; // 数组元素为Base实例,delete[]时回收Base实例内存,调用析构函数

Base **array2 = new Base*[2];
array2[0] = new Base();
array2[1] = new Base();
// 数组元素为Base指针,delete[]时只是回收数组中每一个指针所占用的内存,指针指向的Base实例并没有回收
delete[] array2;
}

不要通过基类数组delete来delete子类数组。以下做法是不合理的:

1
2
3
4
class Base {};
class Derive: public Base {};
Base *pBase = new Derive[3]; // 这句是毫无意义的,因为实际上,pBase并不能访问到子类的任何成员
delete []pBase;

特别的,当析构函数为virtual,并且sizeof(Base)和sizeof(Derive)不一样时,运行时会出错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Base {
public:
int a;
virtual ~Base() {log("in Base dst");}
};

class Derive:public Base {
public:
int b;
virtual ~Derive() {log("in Derive dst");}
};

int main(void) {
Derive *pDerive = new Derive[2];
Base *pBase = pDerive;

cout<<"int size: " <<sizeof(int) <<endl; // 4
cout<<"pointer size: " <<sizeof(int*) <<endl; // 4
cout<<"Base size: " <<sizeof(Base) <<endl; // 8 包括vptr
cout<<"Derive size: " <<sizeof(Derive) <<endl; // 12 包括vptr

delete []pBase; // 出错
}
  • placement new & placement delete
    placement new的作用是在已经分配好的内存上分配内存。placement new的声明定义如下:
    1
    2
    3
    4
    void* operator new(size_t size, void *pLocation) {
    // 作一些检查
    return pLocation;
    }

内存对齐

由于计算机底层内存传输是以字长(32bit机上为4bytes)为单位进行传输的。
例如:

1
2
3
4
5
6
struct X {
char c1;
char c2;
char c3;
};
struct X xarray[2];

如果不进行内存对齐,对于xarray1,存在于第一个字长的最后一个byte和第二个字长的前两个byte。
因此,需要访问xarray1时,需要进行传输两个字长,降低效率。
内存对齐后,则不会有此类问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// sizeof(int): 4
// sizeof(char): 1

struct Data {
int a;
char c;
int b;
};

struct Data1 {
int a;
char c;
int b;

void print() { cout<<"print" <<endl; }
};
// 函数并不影响struct的size。
// 实际上,struct不知道函数,而函数知道struct
// 函数可以放到struct外,写成:
// void print(struct Data1 *this) { cout<<"print" <<endl; }

struct Data2 {
int a;
char c;
int b;
char cc;
};

struct Data3 {
int a;
char c;
char cc;
int b;
};
// 内存对齐时,并不会调整变量的顺序进行优化。

class Data4 {
int a;
char c;
char cc;
int b;
};
// class和struct相同
// 只是class成员默认为private,struct成员默认为public

class Data5 {
int a;
char c;
char cc;
int b;
virtual ~Data5() {}
};
// 含有virtual函数时,还要计算上vptr

int main(void) {
cout<<"size of Data: " <<sizeof(struct Data) <<endl; // 12
cout<<"size of Data1: " <<sizeof(struct Data1) <<endl; // 12
cout<<"size of Data2: " <<sizeof(struct Data2) <<endl; // 16
cout<<"size of Data3: " <<sizeof(struct Data3) <<endl; // 12
cout<<"size of Data4: " <<sizeof(class Data4) <<endl; // 12
cout<<"size of Data5: " <<sizeof(class Data5) <<endl; // 16
}

求值顺序

  • 函数调用顺序
    以下例子在我机子上(win7 & gcc),函数调用时参数求值顺序从后往前的。
    考虑到函数调用时,参数的压栈顺序为,先压最后一个参数,接着倒数第二个…最后压第一个参数。
    大部分情况下,求值顺序为从后往前。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    #include <iostream>

    int func1() {
    std::cout<<"in func1" <<std::endl;
    return 1;
    }

    int func2() {
    std::cout<<"in func2" <<std::endl;
    return 2;
    }

    int func(int, int) {
    std::cout<<"in func" <<std::endl;
    return 3;
    }

    int main(void) {
    func(func1(), func2()); // in func2 -> in func1 -> in func
    func(func2(), func1()); // in func1 -> in func2 -> in func
    return 0;
    }
  • 基本运算的求值

    1
    2
    int a = 1;
    int b = a + (a++);

    以上例子的行为未定义的。C++标准并不明确求职顺序。

  • &&和||求值
    &&和||有最短路径求值。
    (expression A) && (expression B):对A求值,若A为true则对B求值;若A为false则直接返回false而不对B求值
    (expression A) || (expression B):对A求值,若A为true则直接返回true而不对B求值;若A为false则对B求值

引用

引用是变量的另外一个名字。

1
2
3
4
5
6
int main(void) {
int a = 1;
int &refa = a;
refa = 100;
cout<<a; // 100
}

注意:

  • 引用必须在定义的同时初始化
  • 外部(extern)引用不必给初值 extern int *refa;
  • 引用初始化后不能再改为其他变量的引用
  • 引用的地址后被引用变量的地址相同。 int a; int &refa = a; 那么&refa和&a得到的地址相同。
  • 引用和指针一样,可产生多态的效果。
  • 引用的size和指针的size一样。
  • 不能返回局部变量的引用。返回局部变量的引用是不安全的。局部变量在程序离开作用于后即会被回收。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    int& hello() {
    int local = 131;
    return local;
    }

    void test() {
    int x = 100;
    }

    int main(void) {
    int &x = hello();
    std::cout<<x <<std::endl; //输出:131
    test();
    std::cout<<x <<std::endl; //输出:100
    }
    但是如果上面的int &x = hello()改为int x = hello(),那么两次输出的结果都为131。
    原因是返回对local的引用,local的值拷贝到了变量x中。

不能返回函数内部new分配的内存的引用。被函数返回的引用只是作为一个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就无法释放,造成memory leak。

可以返回类成员的引用,但最好是const,否则类内成员变量可以被任意更改,破坏了封装性。

友元

声明友元后,友元能访问类的protected和private成员。

1
2
3
4
5
6
7
8
9
class Base {
friend class Frnd;
private:
int m_x;
};

class Frnd {};

class Derive: public Base {};

此时,Frnd类能访问m_x。
但是friend不能继承,Frnd不能访问Derive的protected和private成员。
并且,如果一个类希望另外的基类和其子类作为友元,必须分别声明。
例如,如果Frnd希望Base和Derive都能访问自己的protected和private成员,必须要

1
2
friend class Base; 
friend class Derive;

位运算

位运算操作符的优先级比+和-低,作运算时要特别注意是否要添加括号。

  • 按位与 &
    可以实现清零,取特定位,保留特定位的效果。
  • 按位或 |
    通常应用于将某些位设定为1
  • 按位异或 ^
    即 0 ^ 0 = 0, 0 ^ 1 = 1, 1 ^ 0 = 1, 1 ^ 1 = 0
    • 使特定位翻转,例如 01111010 ^ 00001111 = 01110101
    • 与0作异或,保留原值
    • 清零,相同值的变量作异或运算得0

利用上一性质,可以得到,变量a和b作两次异或运算结果为a,及时 a ^ b ^ b结果为a
因此,可以实现无临时变量交换值:

1
2
3
a = a ^ b;
b = a ^ b;
a = a ^ b;
  • 按位取反 ~ (一元运算负)
    对于有符号数,易得到 a + (~a) = -1;
  • 左移运算符<<
    左移时,低位补0,高位舍弃。
    左移操作时会改变符号位。

    1
    2
    3
    4
    5
    6
    int main(void) {
    char c = -127; // -127的补码为10000001
    char c2 = (c << 1);
    int x = c2;
    printf("%d", x); // 输出2
    }
  • 右移运算法>>
    右移时,低位舍弃。
    对于高位,当变量为正数时,高位补0;当变量位负数时,补0(逻辑移位)和补1(算术移位)取决于编译器。
    一般情况下是算术移位。

  • 位运算赋值运算符 &=, |=, ^=, >>=, <<=

Rule of Three

  1. 当你需要自己实现析构函数时(通常是因为指针需要delete),通常也需要手动实现copy构造和operator=(通常是为深拷贝)。
  2. 手动实现copy构造,operator=,析构的其中一个函数时,需要考虑另外两个函数是否应该手动实现。
  3. 当类中含有非静态引用或者非静态const变量时必须要自己实现operator=
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Base {
public:
const int constVar;
int &refVar;

Base(int rv, int cv):refVar(rv),constVar(cv) {}
};
// error: non-static const member 'const int Base::constVar', can't use default assignment operator
// error: non-static reference member 'int& Base::refVar', can't use default assignment operator

int main(void) {
int v1 = 1, v2 = 2;
Base b1(v1, v2);

int v3 = 3, v4 = 4;
Base b2(v3, v4);

b1 = b2; // 编译错误
}

继承

公有继承(public)、私有继承(private)、保护继承(protected)是常用的三种继承方式。
下面列出三种不同的继承方式的基类特性和派生类特性。

public protected private
公有继承 public protected 不可见
私有继承 private private 不可见
保护继承 protected protected 不可见

注意以下情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>

class Base {
protected:
int protectedVar;
private:
int privateVar;
};

class Derive: public Base {
public:
void print() {
protectedVar = 1; // 可以访问本类内继承来的protected的protectedVar
//privateVar = 1; // 不能访问
}

void func(Base& base) {
//base.protectedVar; // 不能访问
//base.privateVar; // 不能访问
}

void func2(Derive& derive) {
derive.protectedVar; // 可以访问
//derive.privateVar; // 不能访问
}
};

switch妙用

参考Using {} in a case statement. Why?

case语句中不使用{}可能会导致编译错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
int getNum(int a) {
switch(a) {
case 0:
int x = 1; // error: 'int x' previously declared here & error: redeclaration of 'int x'
return x+2;
break;

case 1:
int x = 2; // error: redeclaration of 'int x'
return x+3;
break;
}
return 1;
}

int getNum(int a) {
switch(a) {
case 0:
int x = 1;
return x+2;
break;

case 1:
x = 2; // error: crosses initialization of 'int x'
return x+3;
break;
}
return 1;
}

给每个case加上{}便不会编译错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int getNum(int a) {
switch(a) {
case 0: {
int x = 1;
return x+2;
break;
}
case 1: {
int x = 2;
return x+3;
break;
}
}

return 1;
}

原因是:

It is possible to transfer into a block, but not in a way that bypasses declarations with initialization.
不能通过case和goto等语句跳过一个含有初始化的声明。switch,case语句实际上是由goto合label实现。

1
2
3
4
5
6
7
8
int main(void) {
goto labely;
labelx:
int a = 1; // error: crosses initialization of 'int a'
// int *a = new int;也是同样的错误
labely:
std::cout<<a;
}

但是可以通过case和goto语句跳过一个不含有初始化的声明。
例如以下两个例子都是没错的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int main(void) {
goto labely;
labelx:
int a;

labely:
a = 11;
std::cout<<a;
}

int getNum(int a) {
switch(a) {
case 0:
int x;
x = 1;
return x+2;
break;

case 1:
x = 3;
return x+3;
break;
}

return 1;
}

那么可以充分利用switch语句的特性,将switch语句和while循环结合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void print(int a) {
int c = 2;
switch(a) {
case 1: do { std::cout<<"case 1" <<std::endl;
case 2: std::cout<<"case 2" <<std::endl;
case 3: std::cout<<"case 3" <<std::endl;
default: std::cout<<"default" <<std::endl;
} while(--c);
}
}

int main(void) {
print(2);
}

// 输出结果为:
// case 2
// case 3
// default
// case 1
// case 2
// case 3
// default