cpp_self_study

查缺补漏,重点整理

引用

这里先只谈左值引用。引用和被引用变量是一回事,个人认为实质上是二者所代表的变量地址相同

函数返回值是引用

例如:

1
2
3
4
5
6
7
8
#include<iostream>
int n;
int &fuck(){return n;}
int main(){
fuck() = 2;
std::cout << n; // 2
return 0;
}

可以将函数调用作为赋值对象。

常引用

指的是 const int &,不能通过常引用去修改其引用的变量。

不能将常引用初始化给非常引用:

1
2
3
const int &p = n;
int &q = p;
// compile error

函数重载

这里重载的定义是:函数名相同,函数参数个数或参数类型不同。

因此,函数参数列表相同但返回值类型不同,是不允许重载的。

如果在类里,还可以通过分别定义非常量成员函数和常量成员函数(函数定义后面加 const )来重载。

例:

1
2
3
int fuck(int n){return n;}
long long fuck(int m){return 1ll * m * m;}
// 1.cpp:4:11: error: ambiguating new declaration of 'long long int fuck(int)'

函数缺省参数

指的是可以给函数参数列表后面几个连续的参数默认值。

1
int fuck(int n, int m = 0){return n + m;}

这里主要理解缺省参数存在的意义,它主要是为了优化程序的可扩充性。在初步进行需求开发时,可能对于一个接口的功能并没有完善的规划。到后期有了更完善的需求,可能会在同一个接口上扩充功能。这时可以在该接口(这里指函数)后面添加缺省参数,通过这个缺省参数来在扩充功能的同时,保证原程序中无需扩充功能的函数调用不需要修改调用形式(即一个一个在函数尾部添加参数)。

然后提醒一下,对于重载+缺省产生的二义性:

1
2
int fuck(int n = 1){return n * 2;}
int fuck(){return 1;}

当使用 fuck() 调用时,产生的二义性会让编译器产生错误。

私有成员

这里主要提醒一件事情,就是对于 private 修饰的成员,只能在成员函数内部访问。但是成员函数内部可以访问自己以及其它相同类的私有成员:

1
2
3
4
5
6
7
8
class fuck{
private:
int shit;
void printshit(fuck *a){
std::cout << shit + a->shit << std::endl;
}
};
// ok

复制构造函数

指的是形如 T (T &)T (const T &) 这样的用另一个对象的引用来初始化该对象的构造函数。如果不自己定义,会自动生成一个默认的复制构造函数。

新增tip:对于封闭类,编译器为其生成默认的复制构造函数时,会按照先调用其成员对象的复制构造函数的规则生成,而不是无参构造函数。

复制构造函数在三种情况下起作用:

  • 用一个对象去初始化另一个对象:Complex c2(c1);Complex c2 = c1; //初始化,不是赋值
  • 一个函数中有一个参数是类A的对象,调用该函数时,类A的复制构造函数会被调用。
  • 函数的返回值是类A的对象,则函数返回时会被调用,将返回值赋值给临时对象(注意)。

当然,像g++这种编译器可能会进行优化,可能不会生成临时对象,就少了中间的临时对象的复制构造函数和析构函数的调用(不愧是g++,主打一个激进)。而msvc这种就会按照C++的规定来编译。

类型转换构造函数

之前没听说过

指的是只有一个参数的构造函数。

这样无论是使用 = 进行赋值还是初始化,可以进行自动类型转换。具体看例子吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<iostream>
class Complex{
public:
double real, imag;
Complex(int i){ // 类型转换构造函数
std::cout << "IntConstructor called" << std::endl;
real = i; imag = 0;
}
Complex(double r, double i){
real = r;
imag = i;
std::cout << "CommonConstructor called" << std::endl;
}
};
int main(){
Complex c1(7, 8);
Complex c2 = 12;
c1 = 9; // 9被转换成一个临时Complex对象,然后赋值给c1,在前面转换的过程中会调用类型转换构造函数
std::cout << c1.real << "," << c1.imag << std::endl;
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
24
25
26
#include<iostream>
class Complex{
public:
double real, imag;
static int total_number;
Complex(int i){ // 类型转换构造函数
std::cout << "IntConstructor called" << std::endl;
real = i; imag = 0;
}
Complex(double r, double i){
real = r;
imag = i;
std::cout << "CommonConstructor called" << std::endl;
}
~Complex(){
std::cout << "Destructor called" << std::endl;
}
};
Complex fun(Complex tmp){
return tmp;
}
int Complex::total_number = 0; // 初始化
int main(){
std::cout << Complex::total_number;
return 0;
}

思考:如何维护这个total_number呢?应该在怎么样的构造函数和析构函数来实时维护该变量?

封闭类构造函数/析构函数

封闭类:含有成员对象的类

封闭类对象生成时,先执行所有对象成员的构造函数(按照类中的说明次序,与初始化列表顺序无关),然后执行封闭类的构造函数。

封闭类对象消亡时,先执行封闭类对象的析构函数,再执行成员对象的析构函数(按照构造函数调用的反序)。

重载自增自减运算符

Tip:C++约定俗成的规则,就是前置形式的 ++c 返回的是对象 c 的引用,c++ 返回的是新的对象。

因此可以这么写 (++c)=1 。重载的时候注意返回值类型。

可以注意到前置的自增自减运算符效率更高,因为后置的情况会导致对象的拷贝。这也是为什么我喜欢在acm中写for循环喜欢写 for(int i = 0; i < n; ++i) ,当然对于内置整形变量其实无所谓了,更看个人风格。

protected

派生类可以访问的是当前对象的基类对象的protected成员,而不能访问非当前对象的protected成员。

多态

主要有两种表现方式:基类指针指向派生类对象、基类引用派生类对象

Tip:在类的成员函数(非构造、非析构)中调用虚函数,等价于 this 指针调用虚函数,表现为多态。而如果是构造函数和析构函数就不是多态(想想也是嘛,多态函数得等对象初始化完才能用)。

Tip2:派生类中和基类的虚函数同名同参数表的函数可以不加 virtual