C++基础
C++面向对象编程的四大特性:
封装:封装是隐藏对象的属性和实现细节,仅对外公开接口,控制程序对类属性的读取和修改。良好的分装能减少耦合,同时隐藏实现细节。
抽象:抽象包括数据抽象和过程抽象。数据抽象关注于目标的特性信息,过程抽象关注于目标的功能是什么,而不是功能怎么实现。
继承:继承是子类继承父类的特征和行为,使得子类具有父类的成员变量和方法。继承可以分为单继承和多继承,单继承一个子类继承一个父类,多继承一个子类继承多个父类。
多态:多态是同一个行为具有不同表现形式或形态的能力。多态的表现形式有重载和覆盖。覆盖是指子类重写从父类继承过来的函数,函数名、返回值、参数列表都必须和父类相同。
继承
- 子类拥有父类的所有属性和行为,子类是特殊的父类;
- 子类对象可以直接当做父类对象使用;
- 子类对象可以赋值或初始化父类对象;
- 父类对象的指针和引用可以指向子类对象;
- 子类对象的指针和引用不能直接指向父类对象,但是可以通过强制类型转换完成。
子类不能继承父类的构造函数,但是可以调用父类的构造函数。子类的构造函数执行顺序:
- 父类的构造函数,按照它们在子类中的先后顺序一次调用;
- 调用成员对象的构造函数,按照它们在类定义中声明的先后顺序一次调用;
- 执行子类构造函数。
如果父类没有定义构造函数,或者使用无参数的构造函数,子类不需要显示的调用父类的构造函数。如果父类没有定义构造函数,子类也可以不定义,全部使用默认的构造函数。如果父类定义了带有参数的构造函数,子类必须定义构造函数,并在初始化列表中显示的调用父类的构造函数。
除了构造函数,父类中定义的友元函数也不能被子类继承,因为友元关系是明确指定的,不会因为继承而传递。同时父类的静态成员和静态函数也无法继承,因为静态成员在整个继承体系中只有一个实体,如果子类中重新定义了该静态成员,子类中定义将覆盖父类的定义,但是并不会影响父类本身。
虚基类
在C++中,虚基类是一种特殊的基类,主要用于解决菱形继承问题。
在菱形继承中,一个类通过两条路径继承了同一个基类,这回导致基类的数据成员在派生类中出现两次,从而引发歧义。为了解决这个问题,我们将共同的基类声明为虚基类。当一个类被声明为虚基类后,无论它被继承层次机构中继承了多少次,它的数据成员在派生类中只出现一次。
class Base {
public:
int data;
};
class Derived1 : virtual public Base {
};
class Derived2 : virtual public Base {
};
class MostDerived : public Derived1, public Derived2 {
};
初始化列表
初始化列表用于构造函数中初始化成员变量。它在构造函数的参数列表之后、函数体之前使用‘:’引出。初始化列表的有点:
- 效率:直接初始化成员变量,而不是先调用默认构造函数再赋值;
- 常量成员:可以初始化const成员变量;
- 引用成员:可以初始化引用成员变量;
- 基类构造:可以调用基类的构造函数。如果基类没有默认构造函数,或者派生类需要调用基类的构造函数初始化基类的私有成员,必须在初始化列表中调用基类的构造函数。
注意:成员变量是按照它们在类中出现的顺序进行初始化的,而不是按照它们在初始化列表中出现的顺序初始化。
多态
在C++中,多态是面向对象编程的一个重要特性,它允许我们使用一个基类的指针或引用来操作派生类对象。多态分为静态多态和动态多态。
静态多态:通过模版和函数重载实现。在编译时,编译器根据参数的类型决定调用的函数。
动态多态:通过虚函数和继承实现。在运行时,根据对象的实际类型决定调用函数。
模版
模板是基于用户为模板参数提供的参数在编译时生成普通类型或函数的构造。(参考)
template <typename T>
T minimum(const T& lhs, const T& rhs)
{
return lhs < rhs ? lhs : rhs;
}
int a = 0;
int b = 1;
int c = minimum(a, b);//mininum<int>(a, b);
上面的代码描述了一个具有单个类型参数 T 的泛型函数的模板,其返回值和调用参数(lhs 和 rhs)都具有此类型。 可以随意命名类型参数,但按照约定,最常使用单个大写字母。 T 是模板参数;关键字 typename 表示此参数是类型的占位符。 调用函数时,编译器会将每个 T 实例替换为由用户指定或编译器推导的具体类型参数。 编译器从模板生成类或函数的过程称为“模板实例化”;minimum<int> 是模板 minimum<T> 的实例化。
这个用例中由于编译器可以根据参数a,b推导出类型,所以可以像普通函数一样调用。
模版参数
模版可以使用类型参数和非类型参数定义。任何内置类型或者用户定义的类型都可以作为类型参数。
类型参数的数量没有实际限制。 以逗号分隔多个参数:
template <typename T, typename U, typename V> class Foo{};
在此上下文中,关键字 class 等效于 typename。 可以将前面的示例表示为:
template <class T, class U, class V> class Foo{};
可以使用省略号运算符 (...) 定义采用任意数量的零个或多个类型参数的模板:
template<typename... Arguments> class vtclass;
vtclass< > vtinstance1;
vtclass<int> vtinstance2;
vtclass<float, bool> vtinstance3;
与其他语言(如 C# 和 Java)中的泛型类型不同,C++ 模板支持非类型参数,也称为值参数。 例如,可以提供常量整型值来指定数组的长度,例如在以下示例中,它类似于标准库中的 std::array 类:
template<typename T, size_t L>
class MyArray
{
T arr[L];
public:
MyArray() { ... }
};
记下模板声明中的语法。 size_t 值在编译时作为模板参数传入,必须是 const 或 constexpr 表达式。 可以如下所示使用它:
MyArray<MyClass*, 10> arr;
其他类型的值(包括指针和引用)可以作为非类型参数传入。 例如,可以传入指向函数或函数对象的指针,以自定义模板代码中的某些操作。
模板可以是模板参数。 在此示例中,MyClass2 有两个模板参数:类型名称参数 T 和模板参数 Arr:
template<typename T, template<typename U, int I> class Arr>
class MyClass2
{
T t; //OK
Arr<T, 10> a;
U u; //Error. U not in scope
};
默认模版自变量
类和函数模板可以具有默认自变量。 如果模板具有默认自变量,可以在使用时不指定该自变量。 例如,std::vector 模板有一个用于分配器的默认自变量:
template <class T, class Allocator = allocator<T>> class vector;
在大多数情况下,默认的 std::allocator 类是可接受的,因此可以使用向量,如下所示:
vector<int> myInts;
但如有必要,可以指定自定义分配器,如下所示:
vector<int, MyAllocator> ints;
对于多个模板参数,第一个默认参数后的所有参数必须具有默认参数。
使用参数均为默认值的模板时,请使用空尖括号:
template<typename A = int, typename B = double>
class Bar
{
//...
};
...
int main()
{
Bar<> bar; // use all default type arguments
}
模版特殊化
在某些情况下,模板不可能或不需要为任何类型都定义完全相同的代码。 例如,你可能希望定义在类型参数为指针、std::wstring 或派生自特定基类的类型时才执行的代码路径。 在这种情况下,可以为该特定类型定义模板的专用化。 当用户使用该类型对模板进行实例化时,编译器使用该专用化来生成类,而对于所有其他类型,编译器会选择更常规的模板。 如果专用化中的所有参数都是专用的,则称为“完整专用化”。 如果只有一些参数是专用的,则称为“部分专用化”。
template <typename K, typename V>
class MyMap{/*...*/};
// partial specialization for string keys
template<typename V>
class MyMap<string, V> {/*...*/};
...
MyMap<int, MyClass> classes; // uses original template
MyMap<string, MyClass> classes2; // uses the partial specialization
只要每个专用类型参数是唯一的,模板就可以具有任意数量的专用化。 只有类模板可能是部分专用。 模板的所有完整专用化和部分专用化都必须在与原始模板相同的命名空间中声明。
函数重载
参考
C++ 允许在同一作用域中的某个 函数 和 运算符 指定多个定义,分别称为 函数重载 和 运算符重载 。
重载声明是指一个与之前已经在该作用域内声明过的函数或方法具有相同名称的声明,但是它们的参数列表和定义(实现)不相同。
当您调用一个 重载函数 或 重载运算符 时,编译器通过把您所使用的参数类型与定义中的参数类型进行比较,决定选用最合适的定义。选择最合适的重载函数或重载运算符的过程,称为 重载决策 。
//函数重载
void print(int i) { cout << i; }
void print(double i) { cout << i; }
//运算符重载
class Box
{
public:
double getVolume(void)
{
return length * breadth * height;
}
void setLength( double len )
{
length = len;
}
void setBreadth( double bre )
{
breadth = bre;
}
void setHeight( double hei )
{
height = hei;
}
// 重载 + 运算符,用于把两个 Box 对象相加
Box operator+(const Box& b)
{
Box box;
box.length = this->length + b.length;
box.breadth = this->breadth + b.breadth;
box.height = this->height + b.height;
return box;
}
private:
double length; // 长度
double breadth; // 宽度
double height; // 高度
};
运算符重载详细内容参照:运算符重载
虚函数
虚函数是C++中的一种机制,它允许在基类中声明一个函数,并在派生类中重新定义该函数。虚函数使得在运行时可以通过基类指针或引用调用派生类的函数,实现多态性。
class Base {
public:
virtual void show() { cout << "base"; }
};
class Derived : public Base {
public:
void show() override { cout << "Derived"; }
}:
int main()
{
Base *b;
Derived d;
b = &d;
b->show(); //call Derived::show()
return 0;
}
虚函数表
每个包含虚函数的类都有一个虚函数表,表中存储了该类的虚函数地址。给个对象都有一个指向其所属类的虚函数表的指针,称为虚表指针(vptr)。
虚函数表的工作原理:
- 当编译器看到类中有虚函数时,会为该类生成一个虚函数表,虚函数表中存储了类的虚函数地址;
- 每个对象都有一个指向所属类的虚函数表的指针vptr;
- 当通过基类的指针或者引用调用虚函数时,程序会通过对象的虚表指针找到虚函数表,然后在虚函数表中查找实际要调用的函数地址。
注意:虚函数表和虚表指针的具体实现时由编译器决定的,不同编译器可能有不同的实现方式。
如果派生类没有重写基类的虚函数,也没有定义新的虚函数,那么派生类将共享基类的虚函数表。只有派生类重写基类的虚函数或定义自己的虚函数,派生类才会有自己的虚函数表。
在C++中,有几种情况不能将函数声明为虚函数:
- 构造函数:构造函数用于初始化对象,而虚函数机制依赖于对象的虚表指针vptr,在构造函数执行时,虚表指针还没有完全初始化。
- 静态成员函数:虚函数是与对象实例相关的,而静态成员函数是与类相关的,不依赖于具体的对象实例。
- 内联函数:虽然技术上可以将内联函数声明为虚函数,但这通常没有意义。内联函数的目的是在编译的时候展开,而虚函数的调用通常在运行时解析,这两者目的相冲突。
- 友元函数:友元函数不是类的成员函数,因此不能参与类的虚函数机制。
纯虚函数
纯虚函数是一种没有实现的虚函数,必须在派生类中重写。纯虚函数的声明方式是在函数声明的末尾加上 ‘= 0’。包含纯虚函数的类称为抽象类,不能实例化对象。
纯虚函数的用途和原因:
- 定义接口:纯虚函数用于定义接口,确保派生类必须实现这些函数。它们提供一种机制,使得类可以定义一些必须在派生类中实现的函数,从而确保派生类具有特定的行为。
- 抽象类:抽象类通常用于定义一组相关类的公共接口,而不提供具体实现。它们用于设计层次机构中的基类。
- 多态性:纯虚函数支持运行时多态。通过基类的指针或引用调用纯虚函数时,实际调用的是派生类的实现,这使得代码更加灵活和可扩展。
- 强制重写:纯虚函数强制派生类提供自己的实现。如果派生类没有实现所有的纯虚函数,那么派生类本身也将成为抽象类,无法实例化。
// 抽象基类 AbstractBase
class AbstractBase {
public:
// 纯虚函数 display,没有默认实现
virtual void display() = 0;
};
// 派生类 Derived 实现抽象基类
class Derived : public AbstractBase {
public:
// 实现抽象基类中的纯虚函数 display
void display() override {
std::cout << "Display function of Derived class" << std::endl;
}
};