Day 1
- 阅读了一些 《C++那些事儿》的基础知识
- Google C++ 命名风格简单学习。
const 关键字
const 对象默认为文件内局部变量
// file1
int v;
extern const int cv;
// file2;
extern int v;
extern const int cv;
std::cout << v << endl;
std::cout << cv << endl;
可以发现未被 const 修饰的变量不需要extern显式声明!而 const 常量需要显式声明 extern ,
并且需要做初始化!因为常量在定义后就不能被修改,所以定义时必须初始化。
const指针,constexpr
- const 类型的指针:
type *const p = &x;
const 值指针指的地址不能变,可以用 p 对 x 作修改。 - 顶层 const 表示指针本身是个常量。
- 底层 const 表示指针所指的对象是一个常量。
constexpr
: 编译时确定是一个常量表达式,来替换 const 。- 与指针搭配,定义的指针将是常量指针(顶层 const),且不能指向在函数中定义的变量(地址不固定)。
- 指向 const 类型的指针可以指向 非 const 类型的对象,但是不能直接修改对象。
static 关键字
声明的数据,空间将在程序生命周期分配
静态变量
静态变量: 函数中的变量,类中的变量
在函数中的静态变量:
- 是每次调用函数共享静态变量
在类中的静态变量:
- 初始化要在类的定义外通过
type Class_name::variable_name = value
来初始化。 - 通常与类的实现写在一起。所有对象共享这个类的静态变量。
静态的类成员
类中的静态对象:
- 在程序结束才会调用析构函数!
类中的静态成员函数:
- 静态成员函数不依赖于类的对象。允许使用对象和'.'来调用静态成员函数。但建议使用类名和范围解析运算符调用静态成员。
Class_name::static_function_name();
- 允许静态成员函数仅访问静态数据成员或其他静态成员函数,它们无法访问类的非静态数据成员或成员函数。
static 用于限制访问范围
static 关键字声明的对象等,限定于本文件的全局变量。
this 指针
this指针的使用:
- 在类的非静态成员函数中返回类对象本身的时候,直接使用
return \*this
。 - 参数与成员变量名相同时,如
this->n = n
(不能写成n = n)。
this 指针相当于给函数传递了一个隐形参数。
-
this 指针会被编译器解析为
Class_name* const this
-
而在 const 函数时,由于const函数只能访问const变量与函数,所以 this 指针是
const Class_name* const this
-
另外,C++ 中 struct 与 class 只有一个区别,类的成员默认是private,而结构是public 。
inline
inline 特点
编译器对 inline 函数的处理步骤:
- 将 inline 函数体复制到 inline 函数调用点处;
- 为所用 inline 函数中的局部变量分配内存空间;
- 将 inline 函数的的输入参数和返回值映射到调用方法的局部变量空间中;
- 如果 inline 函数有多个返回点,将其转变为 inline 函数代码块末尾的分支(使用 GOTO)。
内联能提高函数效率,但并不是所有的函数都定义成内联函数!内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。
- 如果执行函数体内代码的时间相比于函数调用的开销较大,那么效率的收货会更少!
- 另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
以下情况不宜用内联:
-
如果函数体内的代码比较长,使得内联将导致内存消耗代价比较高。(感觉是空间要求高?)
-
如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。
inline 与 虚函数
- 虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。
- 内联是在编译期建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联。
- 所以,
inline virtual
唯一可以内联的时候是:编译器知道所调用的对象是哪个类(如 Base::who()),这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。最终是否内联取决于编译器。编译器能够清楚知道是哪个类的对象调用才可以内联,否则无法在编译期优化
virtual
虚函数与运行多态
- 虚函数的调用取决于指向或者引用的对象的类型,而不是指针或者引用自身的类型。
class A {
public:
virtual void say() {
std::cout << "Hello A\n";
}
};
class B : public A {
public:
virtual void say() {
std::cout << "Hello B\n";
}
};
int main() {
A* p = new B();
p->say(); // 输出 Hello B,但 p 的原始类型是 A
return 0;
}
vptr与vtable
- 每个使用虚函数的类(或者从使用虚函数的类派生)都有自己的虚拟表
- 虚拟表包含可由类的对象调用的每个虚函数的一个条目。此表中的每个条目只是一个函数指针,指向该类可访问的派生函数。
- 虚表只是编译器在编译时设置的静态数组, 编译器还会添加一个隐藏指向基类的指针,我们称之为vptr。vptr在创建类实例时自动设置,以便指向该类的虚拟表。与this指针不同,this指针实际上是编译器用来解析自引用的函数参数,vptr是一个真正的指针。
- 因此,它使每个类对象的分配大一个指针的大小。这也意味着vptr由派生类继承,这很重要。
虚函数中默认参数
- 默认参数是静态绑定的,虚函数是动态绑定的。 默认参数的使用需要看指针或者引用本身的类型,而不是对象的类型。
class A {
public:
virtual void say(int x = 10) {
std::cout << "Hello " << x << std::endl;
}
};
class B : public A {
public:
virtual void say(int x = 20) {
std::cout << "Hello " << x << std::endl;
}
};
int main() {
A* p = new B();
p->say(); // 输出 Hello 10。是 p 的原始类型中的默认参数
return 0;
}
一些常见问题
-
静态函数不可以声明为虚函数
同时也不能被const 和 volatile关键字修饰
虚函数依靠vptr和vtable来处理。vptr是一个指针,在类的构造函数中创建生成,并且只能用this指针来访问它,静态成员函数没有this指针,所以无法访问vptr。
-
构造函数不可以声明为虚函数。同时除了inline|explicit之外,构造函数不允许使用其它任何关键字。
尽管虚函数表vtable是在编译阶段就已经建立的,但指向虚函数表的指针vptr是在运行阶段实例化对象时才产生的。 如果类含有虚函数,编译器会在构造函数中添加代码来创建vptr。 问题来了,如果构造函数是虚的,那么它需要vptr来访问vtable,可这个时候vptr还没产生。 因此,构造函数不可以为虚函数。
我们之所以使用虚函数,是因为需要在信息不全的情况下进行多态运行。而构造函数是用来初始化实例的,实例的类型必须是明确的。 因此,构造函数没有必要被声明为虚函数。
-
析构函数可以为虚函数
析构函数可以声明为虚函数。如果我们需要删除一个指向派生类的基类指针时,
应该把析构函数声明为虚函数。 事实上,只要一个类有可能会被其它类所继承, 就应该声明虚析构函数(哪怕该析构函数不执行任何操作)。 -
虚函数可以为私有函数
基类指针指向继承类对象,则调用继承类对象的函数;
int main()必须声明为Base类的友元,否则编译失败。 编译器报错: ptr无法访问私有函数。 当然,把基类声明为public, 继承类为private,该问题就不存在了