C++ 必知必会
为什么再一次回头看C++
自觉阅读C++相关书籍也已不少。但是有些经典内容值得不断去重复回顾。随着项目开发经验的增加以及代码能力的增强,往常看起来很经典的知识,再一次回顾会产生很多优秀的想法。这是我阅读《C++必知必会》的初衷。这本书我个人认为涵盖C++面试的所有知识,因此,各位可以通过本文来快速了解掌握这本书的干货,对于面试部分更加游刃有余。
C++正文内容
1.数据抽象
- 类型:一组操作。
- 抽象数据类型:一组具有某种实现的操作。
2.多态
- 多态类型:带有虚函数的class类型。
- 多态对象:具有不止一种类型的对象。
- 多态基类:为满足多态对象的使用需求设计的基类。
编写多态的原则:基类提供契约允许针对基类接口编写的多态代码对不同的特定情况起作用,同时对派生类的存在保持“健康的不知情”。
3.设计模式
- 设计模式:一种描述面向对象设计的一种共享通用的术语方式。
4.STL
- STL包括三大组件:容器,算法,迭代器、
(1)容器:容纳组织元素。
(2)算法:执行操作。
(3)迭代器:访问容器中元素。
5.引用是别名而非指针
- 引用与指针的区别
(1)不存在空引用。
(2)所有引用都要初始化。
(3)一个引用永远指向用来对其初始化的对象。 - 注意:一个指向常量的引用采用字面值初始化时,其引用实际被设置为指向“采用该字面值初始化”的一个临时变量。本来临时变量是在表达式末尾就被销毁的。当这类临时对象用于初始化一个常量引用时,在引用指向他们期间,临时对象会一直存在。
6.数组形参
- 退化:C++不存在数组形参,数组传入时,实质只传入指向首元素的指针。
- 一些标准的数组传入方式:
template<int n>
inline void average(int (&array)[n])
{
average_n(array,n);
}
7.常量指针与指向常量的指针
- const T* p1 : 指向常量T的指针。
- T const * p2 : 指向常量T的指针。
- T* const p3 : 常量指针,指向非常量T。
8.指向指针的指针
- 指针数组作为形参时,会退化为指向指针的指针。
- 函数需要改变传递给他指针的值:
(1)将指针p移动到c的位置。
void scanTo(const char** p, char c)
{
while(**p && **p!=c)
{
++*p;
}
}
(2)C++更安全的做法是使用指针引用作为函数形参。
void scanTo(const char*& p, char c)
{
while(*p && *p!=c)
{
++p;
}
}
9.新式转型操作符
- const_cast : 允许添加或移除表达式中类型的const或者volatile。
- static_cast : 可跨平台移植的转型。将一个基类的指针或引用向下转型为一个派生类的指针或引用。
Shape* sp = new Circle;
Circle* cp = static_cast<Circle*>(sp);
- reinterpret_cast : 从bit的角度看待一个对象。允许将一个东西看做完全不同的东西。
- dynamic_cast : 用于执行从指向基类的指针安全的向下转型为指向派生类的指针。仅对于多态类型进行向下转型。(被转型的表达式的类型,必须是一个指向带有虚函数的class类型的指针)同时,执行运行期检查工作。
10.常量成员函数
- 在类X的非常量成员函数中,this指针类型为X *const,即指向非常量X的常量指针。
- 在类X的常量成员函数中,this的指针类型为const X* const,即指向常量X的常量指针。
- 在类X的常量成员函数当中修改类的数据成员,标准做法是将数据成员修改成mutable,而不是const_cast。
11.编译器在类当中放的东西
- 如果一个类声明了一个或多个虚函数,那么编译器将会为该类的每一个对象插入一个指向虚函数表的指针。
- 虚拟继承(virtual inheritance):对象将会通过嵌入的指针,嵌入的偏移或其他非嵌入的信息来保持对其虚基类子对象(virtual base subobject)位置的跟踪。即使类没有声明虚函数,还有可能被插入了一个虚函数表指针。
注意:不管类的数据成员的声明顺序如何,编译器都被允许重新安排他们的布局。 - POD(plain old data):为了避免编译器重新安排他们的布局。使用struct的简单数据结构。
注意:不要使用memcpy标准内存块复制函数来复制对象(用于复制存储区)。 - 对象的初始化或赋值,都会涉及到对象的构造函数,构造函数时编译器建立隐藏机制的地方,该隐藏机制实现对象的虚函数等事物。向未初始化的存储区中塞入一大把比特的做法往往会达到无法预计的结果(比特冲击)。
12.赋值和初始化并不相同
- 赋值和初始化本质上是不同的操作。
(1)对于基础内建类型,赋值和初始化时简单的复制位。
(2)对于用户自定义类型,初始化与赋值的区别如下(以string为例):
String::String(const char* init)
{
if(!init) init = "";
s_ = new char[strlen(init)+1];
strcpy(s_, init);
}
String& String::operator = (const char* str)
{
if(!str) str = "";
char* tmp = strcpy(new char[strlen(str)+1], str);
delete[] s_;
s_ = tmp;
return *this;
}
- 赋值是析构后跟一个构造。
13.复制操作
- 复制构造和复制赋值是两种完全不同的操作。两个操作总是被成对声明。
X(const X&);
X& operator=(const X&);
- 关于实现的两种细节:
(1)T是一个庞大而复杂的类,复制会导致不小的开销。
(2)通过交换X各自实现的指针会极大的加快复制的过程。例如Handle的实现机制。 - Handle class : 句柄类是这样一种类,主要由一个指向其实现的指针构成。
注意:复制构造和复制赋值两个函数是不同的函数,但是两者应该互相兼容,产生的结果不应该有区别。 - 标准的复制赋值实现:
Handle& Handle::operator=(const Handle& that)
{
if(this!=&that)
{
//赋值
}
return *this;
}
14.函数指针
- 声明一个指向特定类型函数的指针:(无需显式取得函数地址,编译器知道隐式获得函数地址,&操作符是可选的。)
void (*fp)(int);
(*fp)(12);
fp(12);
注意:非静态成员函数的地址不是一个指针,因此不可以将一个函数指针指向非静态成员函数。
- 回调:一个可能的动作,这个动作在初始化阶段设置,以便在对将来可能发生的事件作出反应时被调用。
注意:一个函数指针指向内联函数(inline)是合法的,但是通过函数指针调用内联函数不会导致内联函数调用,这是因为编译器无法在编译器精确地确定将会调用什么函数。
15.指向类成员的指针并非指针
- 声明一个指向成员的指针:(使用的是classname:: *)
int C::*pimC;
- 指向成员的指针并不指向一个具体的内存位置,而是一个类的特定成员,而不是特定对象里的特定成员。其是成员在类中的偏移量。
- 存在从指向基类成员的指针到指向公有派生类成员的指针的隐式转换,但是不存在从指向派生类成员的指针到指向其任何一个基类成员的指针转换。
class Shape
{
Point center_;
};
class Circle:public Shape
{
double radius_;
};
Point Circle::*loc = & Shape::center_;
//Shape当中任何偏移量在Circle当中也是一个有效偏移量。
16.指向类成员函数的指针并非指针
- 指向类成员函数的函数指针声明方式:
void (Shape::*mf1)(Point) = &Shape::moveTo;
- 指向一个指向成员函数的指针,通常不能实现为一个简单的指向函数的指针。一个指向成员函数的指针的实现自身必须存储一些信息,诸如它所指向的成员函数是虚拟的还是非虚拟的,到哪里去找到恰当的虚函数表指针。
17.处理函数和数组声明
- 对于函数的声明:
int* f1()