侯捷 C++ 学习路径:面向对象的高级编程 -> STL库 -> C++11新特性 -> cmake
1.1. C 与 C++的区别
在C语言中,主要存在两大类内容,数据和处理数据的函数,二者彼此分离,是多对多的关系。不同的函数可以调用同一个数据,这就导致在开发大型项目的时候,二者纠缠在一起,容易出现问题。
因此推出,面向对象的C++语言,用类将函数和数据进行封装,使得数据只能被某些特定的函数进行处理,效果上类似结构体struct,很好的将数据与函数的多对多关系转变为一对一关系,避免了很多程序错误。
虽然C语言中的 struct 确实可以用来创建复杂的数据类型并在一定程度上模拟面向对象编程(OOP)的思想,但与C++中的 class 相比,C语言的 struct 仍然存在一些重要的限制和区别,这也是为什么C++引入了面向对象的编程概念。以下是一些关键点:
总的来说,虽然在C语言中可以通过 struct 和函数指针等技巧模拟一些面向对象的概念,但C++通过 class 提供了更加完善和直接的支持,使得面向对象编程更加自然和高效。因此,引入C++及其面向对象特性能够大幅简化代码的组织和管理,提升代码的可维护性和扩展性。
1.2. 头文件的防御式声明
在写头文件中代码是,为了避免调用头文件的主文件每次运行都重新读取一遍头文件中的内容,降低程序运行效率,需要我们在头文件中进行防御式声明,如下所示,其中COMPLEX
是我们自己定义的名称,用于区分不同的头文件:
#ifndef __COMPLEX__
#define __COMPLEX__
...
#endif
如此一来,主程序只会在第一次调用头文件时读取头文件的完整内容,避免了反复读取重复内容的操作
1.3. 头文件的布局
#ifndef __COMPLEX__
#define __COMPLEX__
// 前置声明
#include <cmath>
class ostream;
class complex;
complex&
__doapl (complex8* this, const complex& r);
// 类的声明
class complex
{
...
};
// 类的定义
complex::function ...
#endif
1.4. inline(内联)函数
简单来讲,定义为 inline 的函数运行效率更高,但即使你将所有的函数都定义为 inline 函数,也不能保证程序的效率会更好,这是因为一个函数最终是否成为 inline 函数,是由编译器决定的,你的 inline 关键字只是建议。函数若在 class body 内完成定义,便自动成为 inline 的“候选人”。
class complex
{
public:
complex(double r=0,doublei=0)
: re(r),im(i)
{}
complex& operator += (const complex&);
double real () const { return re; }
double imag () const { return im; }
private:
double re,im;
friend complex& __doapl(complex*, const complex&);
};
inline double
imag(const complex& x)
{
return x.image();
}
1.5. access level(访问级别)
私域数据要放在 private 中,方法(函数)根据你是否希望他人看到进行区分,分别放在 public 和 private 部分
1.6. 构造函数和析构函数
- 构造函数不能在类内使用,他是专门为了类的实例化而存在的。
- 不带指针的类多半不用写析构函数,因为不需要手动释放内存。
- 构造函数可以有很多个 - overloading(重载),实际场景中经常用到。
初始化参数列表:complex(double r = 0,double i = 0) : re(r), im(i) {}
,如果不用初始化参数列表对变量进行赋初值,而是在构造函数体内对变量进行赋值,相当于你跳过了初始化参数的过程,虽然大部分情况下对接过没有影响,但程序效率降低。
class complex
{
public:
complex (double r = 0,double i = 0)
: re(r),im(i)
{}
complex(): re(r), im(i) {}
complex& operator += (const complex&);
double real () const { return re; }
double imag () const { return im; }
private:
double re, im;
friend complex& __doapl(complex*, const complex&);
};
void real(double r) { re = r; }
在上面的例子中,我们重载了 real 函数,函数名字相同,但 real 函数编译后的实际名称是不同的,可能如下:
?real@Complex@@QBENXZ
?real@Complex@@AQENABN@Z
// 这个构造函数有两个参数 r 和 i,并且都提供了默认值。这意味着如果不提供参数或提供部分参数,都可以调用这个构造函数。例如:
complex(double r=0,doublei=0) : re(r),im(i) {}
// 这个构造函数没有参数。它是一个典型的默认构造函数,用于在没有提供任何参数的情况下初始化对象。
complex(): re(r), im(i) {}
上面这两种构造函数的方式是冲突的。在C++中,构造函数重载的判别是基于参数的数量和类型。在您的代码中,默认参数的构造函数 complex(double r = 0, double i = 0)
本质上已经涵盖了无参数的情况。
因此,这两个构造函数之间存在歧义,因为编译器不能明确区分它们:
像comple c1;
和 complex c2();
这两种实例化方式都没有传递参数,可以被解释为 complex(double r = 0, double i = 0)
,也可以被解释为Complex()
。导致编译器无法确定在调用构造函数时,应该使用哪个构造函数,因此会导致冲突和编译错误。
1.7. 把构造函数放在 private 区域中
一般情况下,构造函数不会写在 private 区域中,因为这会导致该类无法实例化,但存在特例,即单例模式(Singleton)。
class A {
public:
// 获取单例实例的静态方法
static A& getInstance();
// 一些公有方法
void setup() { ... }
private:
// 私有的构造函数
A();
// 私有的拷贝构造函数
A(const A& rhs);
// 其他私有成员...
};
// 获取单例实例的静态方法实现
A& A::getInstance() {
static A a; // 静态局部变量,确保只会被实例化一次
return a;
}
// 使用单例实例并调用setup方法
A::getInstance().setup();
- 单例模式(Singleton Pattern):单例模式是一种设计模式,它限制一个类只能有一个实例,并提供一个全局访问点。通过单例模式,可以确保一个类只有一个实例,并且该实例易于访问。
- 静态方法 getInstance:这是单例模式的关键方法。通过这个静态方法,可以访问唯一的实例。在该方法内部,定义了一个静态局部变量 static A a;。由于局部静态变量只会在第一次调用时被初始化,因此 a 只会被创建一次,确保了单例的特性。
- 私有的构造函数:构造函数被定义为私有,意味着外部无法直接创建类的实例。这是实现单例模式的关键之一。只有类自身(通过 getInstance 方法)可以访问和创建其实例。
- 私有的拷贝构造函数:拷贝构造函数也被定义为私有,以防止类的实例被复制。单例模式需要确保只有一个实例,因此也需要防止复制行为。
单例模式通过以下方式确保类只有一个实例:
- 控制实例化:通过将构造函数设为私有,禁止外部代码直接创建实例。
- 提供全局访问点:通过一个公共的静态方法(如 getInstance)提供对唯一实例的访问。
- 防止复制:通过将拷贝构造函数和赋值操作符设为私有,防止复制类的实例。
在单例模式之外,构造函数定义在private区域的情况还包括:
- 工厂模式(Factory Pattern):通过工厂方法创建类的实例,而不是直接通过构造函数。
- 控制对象的生命周期:例如,确保对象只在特定条件下被创建。
1.8. 常量成员函数
class 类里面的函数可以分成两种:会改变数据的和不会改变数据的。为保险起见,所有不会改变数据内容的类内成员函数都应该声明为常量成员函数,即:double real () const { return re; }
。
如果类内的成员函数不声明为常量成员函数,会导致函数调用时发生冲突:
const complex c1(2, 1);
cout << c1.real();
cout << c2.image();
上面的代码中,实例化的一个常数类,不能通过调用成员函数修改变量内容,但如果类内成员函数的定义过程中没有加 const
,而是写成了double real () { return re; }
和double imag () { return im; }
,就会导致编译器“丈二和尚摸不着头脑”,不知道到底能不能修改,导致冲突。
1.9. 参数传递:pass by value vs. pass by reference(to const)
// pass by value(值传递,将至本身进行传递,速度较慢,取决于传递值的大小)
complex(double r = 0,double i = 0) : re(r),im(i) {}
// pass by reference(地址传递,速度很快,就是传递一个指针的速度,即四个字节)
complex& operator += (const complex&); //带const,意味着在函数中不能修改complex&类型的变量
最好所有的参数传递都传引用,尽量不要传值,如果传过去但又不希望对方改的话,加上个const
1.10. 返回值传递:return by value vs. return by reference(to const)
complex& operator += (const complex&);
double real () const { return re; }
double imag () const { return im; }
friend complex& __doapl(complex*, const complex&);
- 上面的代码中,第一行和最后一行代码的返回值就是引用。
- 最好所有的返回值传递都传引用,尽量不要传值,如果传过去但又不希望对方改的话,加上个
const
1.11. friend(友元)
一般情况下,数据定义在 private 域中(数据的封装),外部想要拿到需要通过类内 public 域中的函数拿到,但也存在一种特殊情况,我们希望某些“朋友”函数可以直接拿到 private 域中的数据,这时就可以定义友元:
class complex
{
public:
complex(double r=0,doublei=0)
: re(r),im(i)
{}
complex(): re(r), im(i) {}
complex& operator += (const complex&);
double real () const { return re; }
double imag () const { return im; }
private:
double re, im;
friend complex& __doapl(complex*, const complex&);
};
inline complex&
__doapl (complex* ths, const complex& r)
{
ths->re += r.re;
ths->im += r.im;
return *ths;
}
通过友元拿数据要比通过类内函数拿数据更快速,但尽量不要建立太多友元,因为友元实际上是对类的封装的破坏
1.12. 相同类(class)的各个对象(object)互为友元(friend)
class complex
{
public :
complex(doubler=0,doublei=0)
: re(r), im(i)
{ }
int func(const complex& param)
{return param.re + param.im;}
private:
double re,im;
};
{
complex c1(2, 1);
complex c2;
c2.func(c1);
}
1.13. C++编程规范总结
- 所有的数据都要放在 private 域当中
- 参数尽可能通过引用(reference)进行传递,看情况考虑要不要加 const
- 返回值尽量通过引用(reference)进行传递,但存在不能通过引用进行传递的情况
- 在类的 body 内的函数,应加 const 的函数(不改变传入参数和数据的函数)都应该加上
- 构造函数在传参时,尽量使用参数化列表方式进行传递
1.14. class body 外的各种定义
问题:
什么情况下可以 pass by reference
- 只要传入的参数的值不发生改变,就可以 pass by reference
- 修改调用者的变量:如果函数需要修改传入的变量,则使用引用传递。通过引用传递,函数可以直接操作原始变量,而不是其副本。
- 避免复制开销:对于大对象或复杂对象,传递引用可以避免对象的复制开销,提高效率。
- 传递数组:在C++中数组不能直接按值传递,因此通常使用引用或指针来传递数组。
什么情况下可以 return by reference
- 返回的引用必须引用一个有效的对象,而不是局部变量。局部变量在函数返回时会被销毁,返回它们的引用会导致悬空引用。
- 返回类成员:如果函数返回类的某个成员,可以使用引用返回以允许对该成员进行修改。
- 允许链式操作:引用返回可以使得调用者能够连续调用函数,例如常见的链式调用。
- 返回容器元素:当返回容器中的元素时,使用引用返回可以避免复制元素并允许修改元素。
1.15. 操作符重载(operator overloading)
1.15.1. 成员函数
inline complex&
__doapl(complex* ths, const complex& r)
{
ths->re += r.re;
ths->im += r.im;
return *ths
}
inline complex&
complex::operator += (const complex& r)
{
return __dopal (this, r)
}
{
complex c1(2, 1);
complex c2(5);
//c1不发生改变,pass by reference;c2发生改变,pass by pointer
c2 += c1;
}
1.15.2. 非成员函数
上面这三个函数不能 return by reference,因为这三个函数的返回值必定是 local object(函数返回后值被清空)
typename();
这种语法被用来创建临时对象,好处是不用给变量其名字,具体用法如:return complex (real(x) + y, imag(y));
临时对象这种用法平时很少用到,但在标准库中经常用到。
1.16. return by reference 语法分析
先来看一段代码:
inline complex&
__doapl(complex* ths, const complex& r)
{
ths->re += r.re;
ths->im += r.im;
return *ths
}
在这段代码中,我们生命的函数返回值类型是complex&
,是一种引用类型,但函数实际上返回的是*ths
,也就是ths
指针中的内容,这似乎出现了矛盾,但实际上这种写法并没有问题,这是因为:传递者无需知道接受者是以 reference 形式接收的;如果通过 pointer 的形式进行传递,传递者必须知道接受者是以 pointer 形式接收,也就是声明和返回值的类型必须一致,都是指针类型。