访问控制
类的默认访问说明符是
private
,类的默认基类访问也是private
。结构体的默认访问说明符是
public
,结构体的默认基类访问也是public
。
this指针
每个普通的方法调用都会传递一个指向对象的指针,这就是称为“隐藏”参数的
this
指针。
对象生命周期
创建
构造函数没有返回类型,默认构造函数(无参构造函数)没有参数。所有参数都有默认值的构造函数等同于默认构造函数。
在栈中使用构造函数,栈中的对象在声明时会立即调用构造函数。调用默认构造函数不需要使用函数调用的语法!
MyClass myclass(42); MyClass myclass2{42};//使用统一初始化语法 MyClass myclass2();//错误的写法,但能通过编译(编译器将这一行当作函数声明),不会创建对象 myclass2.memberFunc();//编译错误
在自由存储区使用构造函数
auto myclass{make_unique<MyClass>(42)}; MyClass* myclass2{new MyClass(42)}; MyClass* myclass3{nullptr}; myclass3=new MyClass{42};
如果没有定义默认构造函数,则无法创建对象数组。但可以使用初始化器绕过这个限制。
MyClass myclassArray[3];//无法编译 MyClass* myclassArray2 {new MyClass[3]};//无法编译 MyClass myclassArray3[3] {MyClass{42},MyClass{43},MyClass{44}};//可以编译
如果没有声明任何构造函数,那么编译器会自动生成默认构造函数。
显式默认的默认构造函数可以不需要在实现文件中实现,可以避免手动编写空的默认构造函数。
class MyClass{ public: MyClass()=default;//即使声明了其他构造函数,编译器仍然会生成一个默认构造函数 MyClass(int i); };
显式删除的默认构造函数可以用来定义一个只有静态方法的类,这个类没有任何构造函数(包括编译器生成的默认构造函数)。如果类的数据成员是一个对象,其显式删除了默认构造函数却又在声明的时候使用默认构造函数,那么类自己的默认构造函数也会自动删除!
class MyClass{ public: MyClass()=delete; };
构造函数初始化器允许在创建数据成员时执行初始化(赋初值)。C++在创建某个对象时,必须在调用构造函数前创建对象所有的数据成员。如果数据成员本身是对象,那么创建这些对象数据成员时必须为其调用构造函数。在构造函数体中给某个对象数据成员赋值时,并不是创建这个对象(对象已经创建),而只是改变对象的值。数据成员的初始化顺序是它们在类定义中出现的顺序,而不是在构造函数初始化器中的顺序!以下数据成员必须在构造函数初始化器中或者使用类内初始化器进行初始化:
说明 const数据成员 const变量必须在创建时提供初始值 引用数据成员 引用必须在声明时初始化 没有默认构造函数的对象数据成员 C++尝试用默认构造函数初始化成员对象。如果不存在默认构造函数,则必须显式调用它的某个构造函数 基类没有默认构造函数 派生类对象在创建时首先调用基类构造函数,如果基类没有默认构造函数那么必须调用基类的某个构造函数 委托构造函数在构造函数初始化器中调用其他重载的构造函数,当目标构造函数返回时再执行委托构造函数。(在构造函数体中显式地调用其他重载的构造函数实际上新建了另一个临时未命名对象,而不是初始化当前对象)
拷贝构造函数使用另一个对象的const引用作为参数。如果没有编写拷贝构造函数,C++会自动生成一个。C++中传递函数参数默认采用值传递,如果传递的是一个对象,则会调用拷贝构造函数进行初始化。通过将参数作为const的引用来传递,可以避免拷贝构造函数的开销。当函数按值返回对象时,也会调用拷贝构造函数,此时创建一个未命名的临时对象。(std::string_view复制成本很低,可以按值传递;int、double等基本类型应当按值传递)
class MyClass{ public: MyClass(const MyClass& src); };
显式删除拷贝构造函数,对象将无法被复制,这可用于禁止按值传递对象。
初始化列表构造函数将
std::initializer_list<T>
作为第一个参数,并且没有任何其他参数(或者其他参数具有默认值)。单个参数(初始化列表也算)调用的构造函数可用于执行隐式转换,这种构造函数称为转换构造函数。可以使用
explicit(bool)
关键字禁止编译器执行隐式转换。MyClass myclass{42}; myclass={43};//使用43创建一个新对象并赋值给myclass
移动构造函数使用一个临时对象或右值引用作为参数。移动构造函数应使用
noexcept
限定符标记,这是为了与标准库兼容。如果没有声明拷贝构造函数、拷贝赋值运算符、析构函数、移动赋值运算符,那么编译器会自动生成默认的移动构造函数。当函数返回临时对象时,会调用移动构造函数,此时创建一个未命名的临时对象。class MyClass{ public: MyClass(MyClass&& src)noexcept; };
赋值
如果没有编写自己的拷贝赋值运算符,那么编译器会自动生成一个。
拷贝赋值运算符使用另一个对象的const引用作为参数,并且应该返回对象本身的引用。
class MyClass{ public: MyClass& operator=(const MyClass& rhs); private: int m_value{42}; }; MyClass& MyClass::operator=(const MyClass& rhs){ if(this==&rhs) return *this;//检查自我赋值 m_value=rhs.m_value; return *this; }
在C++中,“复制”只在初始化对象时发生,使用拷贝构造函数。
在C++11中,如果用户声明了拷贝赋值运算符或者析构函数,编译器将不会生成拷贝构造函数。同样,如果用户声明了拷贝构造函数或者析构函数,编译器将不会生成拷贝赋值运算符。
拷贝构造函数和拷贝赋值运算符中必须进行深拷贝。
移动赋值运算符使用一个临时对象或右值引用作为参数。移动赋值运算符应使用
noexcept
限定符标记,这是为了与标准库兼容。如果没有声明拷贝构造函数、拷贝赋值运算符、析构函数、移动构造函数,那么编译器会自动生成默认的移动赋值运算符。class MyClass{ public: MyClass& operator=(MyClass&& rhs)noexcept; };
销毁
- 销毁对象时,会调用析构函数,释放对象占用的内存。如果没有声明析构函数,那么编译器会自动生成一个。析构函数会逐一销毁成员(数据成员的销毁顺序与它们的声明顺序相反),然后允许释放对象。析构函数会被隐式标记为
noexcept
。- 栈中的对象在离开作用域时会自动销毁。
- 在自由存储区中分配的对象不会自动销毁,需要使用
delete
或者delete[]
手动销毁。
类的成员
成员函数(方法)
静态方法
- 静态方法不属于特定对象,因此没有this指针。
- 静态方法只能访问对象的静态成员。
- 当使用对象调用静态方法时,C++不关心对象实际上是什么,只关心编译期的类型。
const方法
- 使用const关键字标记方法本身,会使方法不能改变任何数据成员。因为方法内用到的数据成员都被标记为const引用。
- 非const对象可以调用const方法和非const方法,const对象只能调用const方法。
- 使用
mutable
修饰数据成员,使其在const方法中可以被改变值。
方法重载
可编写两个名称相同、参数也相同的方法,其中一个是const,另一个不是。通常情况下,const版本与非const版本的实现是一样的,为避免代码重复,可以正常实现const版本,非const版本通过调用const版本实现(不能反过来)。
class MyClass{ public: std::string& getStr(); const std::string& getStr()const; private: std::string m_str{}; }; std::string& MyClass::getStr() { return const_cast<std::string&>(std::as_const(*this).getStr());//as_const(obj)等价于const_cast<const T&>(obj),T是obj的类型 } const std::string& MyClass::getStr()const { return m_str; }
使用
&
限定符可以使方法只能被非临时实例调用,使用&&
限定符可以使方法只能被临时实例调用。class MyClass{ public: void print(const std::string&)&; void print(const std::string&)&&; }; void MyClass::print(const std::string& str)& { cout << "只能被非临时实例调用 " << str << endl; } void MyClass::print(const std::string& str)&& { cout << "只能被临时实例调用 " << str << endl; } int main() { MyClass myclass; myclass.print("myclass"); MyClass{}.print("temp"); move(myclass).print("move"); };
成员变量(数据成员)
静态数据成员
可将静态数据成员声明为
inline
,这样就不用在源文件中为它们分配空间。class MyClass{ private: static inline size_t ms_counter{0}; };
成员枚举
- 枚举类型可作为类的数据成员。
嵌套类
- 可在类定义中提供另一个类定义。嵌套类可用来创建接口。
友元
可以将类、类中的某个方法、函数作为另一个类的友元,友元可以访问类的
protected
、private
数据成员和方法。class Foo{ friend void Bar::processFoo(const Foo&); };
右值引用
左值是可获取其地址的一个量。不是左值的量都是右值(字面量、临时对象、临时值)。
右值引用是一个对右值的引用。这是一个当右值是临时对象或者使用
std::move()
显式移动时才适用的概念。右值引用的目的是在涉及右值时提供特定的重载函数。通过右值引用,某些涉及复制大量值的操作可以通过简单地复制指向这些值的指针来实现。
通常,临时对象被当作
const type&
。有名称的变量是左值。有名称的右值引用,如右值引用参数本身是左值。
可以声明右值引用类型的变量
int& i{2};//错误,不能引用常量 int a{3},b{4}; int& j{a+b};//错误,不能引用临时值 int&& k{2};//代码合法 int&& l{a+b};//代码合法
如果将临时值赋值给右值引用,则只要右值引用在作用域内,临时值的生命周期就会延长。
移动语义
对象的移动语义需要实现移动构造函数和移动赋值运算符。只有当源对象不再被使用时,移动语义才有用。
对于
return object;
形式的语句,如果object是局部变量、函数的参数或者临时值,则它们被视为右值表达式,并触发返回值优化(RVO)。如果object是局部变量,则会启动命名返回值优化(NRVO)。因此,当返回值是局部变量或者函数参数时,只要写return object;就可以了,不要写std::move(object);,因为如果object不支持移动语义,那么编译器将使用复制语义。return condition?object1:object2;//这不是return object;的形式,编译器不会应用(N)RVO //改成以下写法 if(condition) return object1; else return object2;
如果方法的参数类型支持移动语义,那么可以按值传递参数,当传递左值时将其复制到参数,传递右值时将其移动到参数。这样就不用为方法分别编写const引用版本和右值引用版本。
继承
基类的指针或引用可以指向派生类的对象,但只能使用基类的方法和被派生类重写的方法,无法使用派生类独有的方法。
将派生类对象赋值给基类对象,此时基类对象就是基类对象。
在类名后面使用
final
关键字可以禁止这个类被继承。基类中声明为
virtual
的方法才能被派生类正确的重写。重写时删除virtual
关键字和添加override
关键字。如果类中有虚方法,那么生成的实例中会包含一个指向虚表的指针。虚表包含指向虚方法实现的指针。
某些情况下(基类的指针指向派生类的对象),销毁一个派生类对象时有可能调用的是其基类的析构函数而不是派生类的析构函数。因此需要将析构函数声明为virtual。
将方法标记为
final
可以禁止这个方法被派生类重写。对象的创建顺序:调用基类的构造函数——初始化数据成员——调用自己的构造函数。从基类构造函数中调用虚方法,会调用虚方法的基类实现而不是派生类的重写版本。
析构函数的调用顺序与构造函数相反:调用自己的析构函数——销毁数据成员(销毁顺序与创建顺序相反)——调用父类的析构函数。从基类析构函数中调用虚方法,会调用虚方法的基类实现而不是派生类的重写版本。
即使子类重写了父类的方法,该方法的父类版本仍然存在,仍然可以被使用。
如果要进行向下转型,应该使用
dynamic_cast<T*>()
,且对象要具有虚表。如果派生类没有实现从基类继承的所有纯虚方法,那么派生类也是抽象的。
多重继承要注意消除歧义。
可使用using关键字显式地包含基类中定义的方法(所有重载的方法都会被继承)。
class Base{ public: Base(); Base(int i); }; class Derived:public Base{ public using Base::Base;//继承基类中所有的构造函数(除了默认构造函数) };
静态方法不能被重写。如果派生类中有静态方法与基类的静态方法同名,那么这实际上是两个独立的方法。
可以重写基类的private方法。
重写具有默认参数的方法时,也应该提供默认参数。
如果派生类中指定了拷贝构造函数,就需要显式地链接到父类的拷贝构造函数。否则创建派生类对象时将调用父类的默认构造函数而不是拷贝构造函数。
class Base{ public: Base()=default; Base(const Base& src){} }; class Derived:public Base{ public: Derived(const Derived& src):Base{src}{} };
如果派生类重写了operator=,则几乎总是需要调用父类版本的operator=。
类型转换
static_cast()
用于基本类型之间的转换,不会执行运行时类型检查。不能将指针转换为另一个不相关的指针;不能将对象转换为另一种类型的对象(除非有可用的转换构造函数);不能将常量类型转换为非常量类型;不能将指针转换为整数。reinterpret_cast()
允许在不执行任何类型检查的情况下进行转换。可以用来将void类型的指针强制转换回正确类型的指针。dynamic_cast()
可以用来强制转换指针或引用,提供运行时类型检查。如果转换无效,将返回nullptr
(对于指针),或者抛出std::bad_cas
t异常(对于引用)。对象运行时的类型信息存储在vtable中,这要求类中必须至少有一个虚方法,才能使用dynamic_cast()
。std::bit_cast()
会创建一个指定目标类型的新对象,按位从源对象复制到新对象。要求源对象与目标对象大小相同,并且都是可复制的。