一、面向对象编程
(一) 面向过程vs面向对象
面向过程(Procedural-Oriented-Programming, POP)和面向对象(Object-Oriented-Programming,OOP),是两种典型的编程范式,通常是作为划分编程语言的一大依据,在许多现代编程语言中都会有面向对象的编程范式,这是因为其与现实世界的良好契合。
C是面向过程的典型,C++具有面向对象的编程范式,这就意味着它们的程序设计思想会有所不同(尽管,你仍然能够在C++使用C的编程思想,但是这就浪费OOP的设计优势)。
1、C语言怎么解决问题
假设我们需要编写简单的程序来管理一些银行账户,包括存款和取款的功能。在C语言中,我们通常会使用全局变量或传递指针来访问和修改数据。
#include <stdio.h>
// 定义账户结构体
typedef struct {
char name[50];
double balance;
} Account;
// 函数声明
void deposit(Account *account, double amount);
void withdraw(Account *account, double amount);
int main() {
// 创建一个账户实例
Account myAccount = {"John Doe", 1000.0};
// 存款操作
deposit(&myAccount, 500.0);
// 取款操作
withdraw(&myAccount, 200.0);
printf("Account Balance: %.2f\n", myAccount.balance);
return 0;
}
void deposit(Account *account, double amount) {
account->balance += amount;
}
void withdraw(Account *account, double amount) {
if (account->balance >= amount) {
account->balance -= amount;
} else {
printf("Insufficient funds.\n");
}
}
在这个示例中,我们定义了一个Account结构体来存储账户信息。然后我们通过函数deposit和withdraw来操作这个结构体。这种方法将数据和行为分离,数据由结构体表示,而行为由独立的函数实现。
2、C++怎么解决问题
在C++中,我们可以使用类和对象来将数据和行为封装在一起。这样可以使代码更加模块化和易于维护。
#include <iostream>
#include <string>
class Account {
public:
// 构造函数
Account(const std::string& name, double initialBalance)
: name(name), balance(initialBalance) {}
// 存款方法
void deposit(double amount) {
balance += amount;
}
// 取款方法
bool withdraw(double amount) {
if (balance >= amount) {
balance -= amount;
return true;
} else {
std::cout << "Insufficient funds." << std::endl;
return false;
}
}
// 获取余额
double getBalance() const {
return balance;
}
private:
std::string name;
double balance;
};
int main() {
// 创建一个账户实例
Account myAccount("John Doe", 1000.0);
// 存款操作
myAccount.deposit(500.0);
// 取款操作
myAccount.withdraw(200.0);
std::cout << "Account Balance: " << myAccount.getBalance() << std::endl;
return 0;
}
在这个面向对象的例子中,我们定义了一个Account类,其中包含了账户的数据成员(name和balance)以及与这些数据相关的成员函数(deposit、withdraw和getBalance)。这样的设计允许我们通过对象来直接调用方法,从而将数据和行为紧密地关联在一起。
(二) 类与对象
类是面向对象编程中的一个概念,它是一种创建对象的蓝图或模板。类定义了对象的属性(数据成员)和行为(成员函数),它们描述了对象的特征和能力。
值得注意的是,这里的术语“对象”与之前所说的“对象”(内存空间)是不同的,这里的“对象”是指由一组属性和一组操作所构成的封装体。描述了现实世界或计算机系统中一个物理的或逻辑的事物,其属性和操作分别描述了事物的静态特征和动态特征。
通过类可以创建多个对象,每个对象都是该类的一个实例(instance),具有相同的属性和方法。
在面向对象程序设计中,使用数据成员模拟对象的属性。在面向对象程序设计中,成员函数是模拟对象行为的函数。
在C++中类是用户定义类型,具有以下定义形式
class | struct ClassName{
AccessSpecifier: // public, private, protected
DataType VariableName; // data type and variable name
// Constructor
ClassName(DataType var){ // constructor body
}
// Destructor
~ClassName(){ // destructor body }
}
// Member Functions
void FunctionName(DataType var){ // function body
}
};
在很多时候,我们总是可以自由选择struct和class,但是,在后面读者会发现它们之间的区别,以选择正确的声明。
虽然在定义用户自定义的类型时可以自由使用小写或者大写字母,但是我们遵循一个惯例,即类名以大写字母开头,以将它们与以小写字母开头的库中的类区分开来。
类体是一个语句块(从左花括号开始,到右花括号结束),它包含数据成员和成员函数的声明。类定义的第一部分总是声明类的数据成员:内置类型或者其他先前定义的类类型的变量或者常量。
类中的数据成员不能相互依赖。在任何对象中,我们都可能有若干属性,其中一些属性依赖于其他属性,并且可以根据其他属性进行计算。在依赖属性中,我们需要选择最简单和最基本的属性。
例如,在圆的类中,我们有三个属性:半径、面积和周长。给定其中一个属性,另外两个属性可以通过给定的属性来计算。因为半径是最原始和最基本的,因此选择半径作为数据成员。在依赖属性中选择多个属性可能会导致程序出错:我们可能会更改其中一个属性,而忘记更改其他属性。例如,如果我们同时选择了半径和面积作为数据成员,则使用成员函数更改半径而不同时更改面积,将创建一个面积计算错误的圆,反之亦然。
访问修饰符(access modifier) 用于确定类的可访问性。类中数据成员和成员函数的声明在默认情况下是私有的,无法访问私有数据成员和私有成员函数以进行检索或者更改。
C++定义了三种访问修饰符,类的设计者可以将访问修饰符应用于成员的声明,以控制对该成员的访问。
数据成员的访问修饰符通常设置为private,以强调其私有性(尽管没有修饰符也表示 private)。要对数据成员进行操作,应用程序必须使用成员函数,这意味着成员函数的声明通常必须设置为 public。
分组访问修饰符。我们在整个类定义中只使用简单几个修饰符,比如一个 private关键字和一个public 关键字。这被称为分组访问修饰符。
换言之,当前访问修饰符在我们遇到新的访问修饰符之前是一直有效的。我们可以为每个数据成员或者成员函数定义一个访问修饰符并后跟冒号,但这不是必需的。访问修饰符后面的缩进是为了使代码更清晰。
(三) 类类型
C++中的类类型是使用类修饰符定义的用户自定义类型,它具有以下声明语法
class-key attr (optional) class-head-name final(optional) base-clause (optional) { member-specification }
class-key attr (optional) base-clause (optional) { member-specification }
第一种,具有类名,第二种没有,这里不讨论。
对于第一种,class-key可以是class\union\struct中任意一种,这就意味着,C++中存在三种类,但是在大多数情况下,包括C++标准库,一般都是选择class,这不是约定俗成,而是使用不同的class-key是具有不同的限制的,它们的区别在下面依次说明
二、构造和析构
构造函数是用于创建和初始化对象的特殊成员函数。析构函数是用于清理和销毁对象的特殊成员函数。
三种class-key声明的类都具有构造和析构函数
union someUnion {
int data;
someUnion(){
data = 0;
std::cout << "Union Constructor called" << std::endl;
}
~someUnion(){
std::cout << "Union Destructor called" << std::endl;
}
void print() {
std::cout << "data: " << data << std::endl;
}
};
struct someStruct {
int data ;
someStruct(){
data = 0;
std::cout << "Struct Constructor called" << std::endl;
}
~someStruct(){
std::cout << "Struct Destructor called" << std::endl;
}
void print() {
std::cout << "data: " << data << std::endl;
}
};
class someClass {
int data;
someClass(){
data = 0;
std::cout << "Class Constructor called" << std::endl;
}
~someClass(){
std::cout << "Class Destructor called" << std::endl;
}
void print() {
std::cout << "data: " << data << std::endl;
}
};
(一)构造函数
构造函数是一个成员函数,它在被调用时创建对象,并在被执行时初始化对象的数据或员。类定义中数据成员的声明不会初始化数据成员,声明只给出数据成员的名称和类型。
构造函数有两个特征:它没有返回值; 其名称与类的名称相同。
构造函数不能有返回值(甚至不能是void),因为它不是为返回任何内容而设计的,其用途不同。构造函数用于创建一个对象并初始化该对象的数据成员。尽管我们会看到一个构造函数也可以执行一些其他任务,例如值的验证,但这些任务也被认为是初始化的一部分。
在一个类中, 可以包含三种类型的构造函数: 参数构造函数( parameter constructer)、默认构造函数(default constructor)和拷贝构造函数(copy constructor)。
1、构造函数的声明
构造函数是类的成员函数,这意味着它必须在类定义中声明。构造函数没有返回值,某名称与类的名称相同。
参数构造函数。通常类定义中会包含一个参数构造函数,它用指定的值初始化每个实例的数据成员。参数构造函数可以重载,这意味着我们可以有多个参数构造函数,每个参数构造函数具有不同的签名。
默认构造函数。默认构造函数是不带参数的构造函数。它用于创建对象,把对象的所有数据成员设置为字面量值或者默认值。注意,我们不能重载默认构造函数。因为默认构造函数没有参数列表,所以不适合重载。
拷贝构造函数。有时候,我们希望将对象的每个数据成员初始化为与先前创建的对象的相应数据成员相同的值。在这种情况下,我们可以使用拷贝构造函数。
拷贝构造函数将给定对象的数据成员值复制到刚刚创建的新对象中。在调用拷贝构造函数之后,源对象和目标对象的每个数据成员具有完全相同的值,尽管它们是不同的对象。拷贝构造函数只有一个参数,按引用接收源对象。
参数类型前面的 const修饰符保证按引用传递时不能更改源对象。请记住,按引用传递有两个特征:第一,不需要物理复制对象; 第二,改变目标对象意味着同时会改变源对象。使用const修饰符后,我们保留第一个特征,但禁止第二个特征。还要注意,我们不能重载拷贝构造函数,因为参数列表是固定的,并且不能有替代形式。
2、构造函数的定义
这可能很奇怪,在上面我已经将三个类的构造函数定义了,那为什么还要讲定义呢?那是因为一般来说,我们只在类定义时,声明函数,而不实现。如果只在类定义时声明,那么在类外定义函数就要注意了,以特殊函数为例,其他函数也是。
union someUnion {
int data;
// 在联合声明时只声明函数
someUnion() ;
~someUnion();
void print() ;
};
// 在联合声明后定义函数
someUnion::someUnion() {
std::cout << "Union Constructor called" << std::endl;
}
someUnion::~someUnion() {
std::cout << "Union Destructor called" << std::endl;
}
void someUnion::print() {
std::cout << "data: " << data << std::endl;
}
这类似于我们认识两个叫苏的人,一个来自布朗家,另一个来自怀特家。在C++中,我们需要先提到类名(姓氏),接着是类作用域符号(::) 来实现这个目标。
读者可能会提出疑问,我们为什么没有在类定义中包含数据成员或者成员函数的姓氏原因是这些成员包含在类定义中,它们属于该类。这类似于当一个人在家里时,我们不需要提到他的姓氏, 当他不在家庭圈中时,则需要使用姓氏。
注意,函数的返回类型总是在整个名称之前。在类定义中,没有显式的姓氏,因此函数的返回类型出现在函数名之前。在函数定义中,函数的返回类型必须在整个名称之前。
任何类成员的名称仅能在以下四种上下文中使用:
- 在其自身的类作用域或派生类的类作用域中
- 在应用于其类类型的表达式或从其派生的类类型的表达式之后的(.)运算符之后
- 在应用于指向其类的指针类型的表达式或指向从其派生的类的指针类型的表达式之后的(->)运算符之后
- 在应用于其类的名称或从其派生的类的名称之后的(::)运算符之后
3、初始化列表
构造函数定义和其他成员函数定义的主要区别在于,构造函数可以在函数头后面有一个初始化列表来初始化数据成员。
初始化列表放在构造函数头之后和构造函数主体之前,并以冒号开头。
我们可以将每个初始化看作一个赋值语句,用于将参数赋值给数据成员,例如 dataMember = parameter。初始化列表没有终止符。
下一行是构造函数的主体。数据成员的名称必须与数据成员声明中定义的名称相同,但每个参数的名称由程序员确定。
另一个要点是,创建对象时必须初始化对象的常量数据成员。常量实体被声明之后,就不能被修改,但是C++允许我们在构造函数的初始化部分初始化对象的常量数据成员。
然而,有时我们必须使用构造函数的主体来初始化复杂的数据成员(通过赋值),这些成员不能在初始化列表中简单地被初始化。构造函数的主体还可以用于其他处理,例如验证参数,根据需要打开文件,甚至打印消息以验证是否调用了该构造函数。
(二)析构函数
与构造函数类似,析构函数也有两个特殊的特性:首先,析构函数的名称是以波浪线符号(~) 开头的类的名称,但是波浪线被添加到名字中,而不是姓氏中(所有成员函数的姓氏都相同); 其次,与构造函数一样,析构函数不能有返回值(甚至不能是 void),因为它什么也不返回。
当类的实例化对象超出其作用域时,系统保证自动调用和执行析构函数。换言之,如果我们实例化了类的5个对象,则会自动调用析构函数5次,以确保所有对象都被清理。如果对象构造时调用了诸如文件之类的资源,则清理工作十分重要。
程序终止后,分配的内存将被回收。析构函数不能接收任何参数,这意味着析构函数不能被重载。
(三)default与delete
对于之前所说的特殊成员函数,如果不显式提供,那么编译器就会自动提供
可以显式声明使用默认提供的函数定义,这点在之前提到过
注意无参构造不可重载
声明为默认后,不要再定义
Explicitly-defaulted functions and implicitly-declared functions are collectively called defaulted functions. Their actual definitions will be implicitly provided
显式默认函数和隐式声明函数共同被称为默认函数。它们的实际定义将被隐式提供。
也可以不使用默认提供的,或者说禁止某类构造函数(显式删除)
任何对已删除函数的使用都是格式错误的(程序将无法编译)。这涵盖了各种情况,包括显式的调用(通过函数调用运算符)和隐式的调用(对已删除的重载运算符、特殊成员函数、分配函数等的调用),创建指向已删除函数的指针或指向成员的指针,甚至在未可能求值的表达式中使用已删除函数。
(四)默认访问修饰符
我已经提到过C++提供了三种访问修饰符,这三种在三种类中都存在,不过struct\union默认public,而class默认private
这说明class对内部保护更加严格
三、实例成员
实例成员是指在面向对象编程中,与类的实例(即对象)相关联的成员。这些成员包括字段(属性)、方法等,它们不属于类本身,而是属于通过类创建的每一个具体对象。实例成员的特点是它们依赖于类的实例而存在,每个实例对象都有自己独立的实例成员副本。
(一) 实例数据成员
实例数据成员定义对象实例的属性,这意味着每个对象必须封装在类中定义的数据成员集。这些数据成员仅属于相应的实例,其他实例无法访问。这里的术语“封装”意味着为每个对象分配单独的内存区域,并且每个区域为每个数据成员存储不同的值。
(二) 实例成员函数
实例成员函数,定义可以应用于对象的实例数据成员的行为。尽管每个对象都有自己的实例数据成员,但内存中每个实例成员函数只有一个副本,必须由所有实例共享。
与实例数据成员不同,实例成员函数的访问修饰符通常是公共的,并且允许从类外部(应用程序) 进行访问,除非实例成员函数仅由类中的其他实例成员函数使用。
1、函数调用问题
应用程序(例如,main 函数) 可以调用实例成员函数对实例进行操作。在面向对象程序设计中,此调用必须通过实例完成。应用程序必须首先创建一个实例,然后让该实例调用实例成员函数。换言之,这有点类似于实例是在对自身进行操作。C++语言为此定义了两个运算符,称为成员选择运算符。
-> | pointer -> membe |
如果一个成员函数只有一个副本,那么该函数如何在一个时间被一个对象使用,而在另一个时间被另一个对象使用?更重要的问题是,当一个对象正在使用一个函数时,我们如何阻止其他对象使用该函数?换言之,在函数被使用时我们如何将其锁定,在函数终止(返回)时又如何将其解锁,以便以后其他对象可以使用该函数?
答案是:在C++的后台完成锁定和解锁。C++将一个指针(一个保存对象地址的变量)添加到每个成员函数中。因此, 当我们使用点成员选择运算符时,编译器将其转换为指针成员选择运算符,每个成员函数都有一个名为this 指针的隐藏指针。函数由 this 指针指向的对象使用。换言之, 函数代码被应用于 this 指针指向的对象的数据成员。
隐藏参数。实例成员函数如何获取 this 指针呢?编译器会将 this 指针作为参数添加到实例成员函数中,如下所示:
//用户编写的代码 // 编译器转换后的代码
double getRadius() const double getRadius (Circle*this) const
{ {
return radius; return(this -> radius);
} }
为此,不允许自定义使用
2、宿主对象
当执行实例成员函数时,总是有一个宿主对象( host object)。宿主对象是实例成员函数在给定时刻所操作的对象。换句话说,宿主对象就是 this指针指向的对象。实例成员函数执行期间只有一个宿主对象。
3、getter和setter
访问器成员函数(有时称为getter) 从宿主对象获取信息,但不更改对象的状态。换言之,访问器成员函数使宿主对象成为只读对象。它获取一个或者多个数据成员的值,但不更改其在宿主对象中的值。
getter通常具有以下形式:
返回类型 get成员变量名() const {
return 成员变量名;
}
其中,返回类型
是getter函数的返回类型,get成员变量名()
是getter函数的名称,const
关键字表示该函数不会修改对象的状态,成员变量名
是要获取的私有成员变量的名称。
访问器成员函数可以不必返回数据成员的值。也可以使用访问器成员函数来创建副作用(例如,输出值),只要对象的状态保持不变。
const修饰的实例函数不允许修改数据成员
此外,并非只有getter函数才能有const修饰,除了特殊函数都可以添加,如果不希望函数内部修改数据成员
程序中类类型的对象通常由参数构造函数初始化。这意味着对象的状态是在构造时设置的。然而,有时我们必须改变原始状态。例如,如果我们创建一个表示银行账户的类,则表示余额的数据成员将随时间变化(每次存款和取款)。这意味着我们可能需要实例成员函数来更改其宿主对象的状态。这样的函数称为更改器成员函数(有时称为setter)。此函数不能有常量限定符const,因为它需要更改宿主对象的状态。
setter通常具有以下形式:
void set成员变量名(参数类型 参数名) {
成员变量名 = 参数名;
}
其中,void
表示setter函数没有返回值,set成员变量名(参数类型 参数名)
是setter函数的名称,成员变量名
是要设置的私有成员变量的名称,参数类型
是要传递给setter函数的参数类型,参数名
是要传递给setter函数的参数名称。
更改器实例成员函数不一定需要通过参数来更改数据成员的值,它可以是一个不带参数但具有副作用(例如,通过输入一个值) 的函数。
通过使用getter和setter,可以实现对类的私有成员变量的封装和控制访问,从而提高代码的可维护性和安全性。
(三) 类不变式
C++类不变式是指在一个类的对象上的某个属性在其整个生命周期中保持不变的条件。类不变式通常通过成员函数和类的私有成员来确保。类不变式的目的是保证对象的一致性和正确性。
在C++中,常用的方式是在类的私有成员中定义类不变式,并在类的公有成员函数中进行检查和维护。例如,假设有一个表示矩形的类Rectangle,其私有成员包括矩形的宽度和高度。那么,可以通过以下方式定义矩形的类不变式:
class Rectangle {
private:
int width;
int height;
bool isInvariant() {
// 检查类不变式是否满足
return width > 0 && height > 0;
}
public:
Rectangle(int w, int h) : width(w), height(h) {
assert(isInvariant()); // 构造函数中检查类不变式
}
void setWidth(int w) {
assert(w > 0); // 设置宽度时检查前置条件
width = w;
assert(isInvariant()); // 每次修改属性后检查类不变式
}
void setHeight(int h) {
assert(h > 0); // 设置高度时检查前置条件
height = h;
assert(isInvariant()); // 每次修改属性后检查类不变式
}
};
在上述示例中,类不变式width > 0 && height > 0
被定义为私有成员函数isInvariant()
。在类的构造函数中和每次修改属性后,都通过assert()
函数来检查类不变式是否满足。如果类不变式不满足,程序就会终止并打印错误信息。
通过类不变式,可以确保对象在其整个生命周期中保持一致性和正确性。如果类不变式被破坏,就可能会导致程序错误和异常行为。因此,在设计和实现类时,合理定义和尽可能强制类不变式是很重要的。
四、类成员(静态成员)
类成员是指在面向对象编程中,定义在类定义中的变量和方法。这些成员可以是数据成员(也称为属性),用于存储数据;或者是成员函数(也称为方法),用于定义类的行为。类成员可以是静态的(属于类本身,而不是类的任何特定实例),也可以是实例成员(属于类的特定实例)。
(一) 静态数据成员
静态数据成员是所有类对象共享的,也就是说,不论创建多少个对象,静态数据成员只有一个副本。它可以在类外初始化,并且必须显式初始化。
对于非常量静态数据成员,必须在类外初始化
否则也是可以在类中进行初始化,不过就不可更改了
对于在类外初始化的静态数据成员
要保持和类中一样的声明方式,并且加上类名,表示所属
(二) 静态成员函数
在声明和初始化静态数据成员之后,我们必须寻求访问静态数据成员的方法(例如,打印它的值)。因为静态数据成员通常是私有的,所以我们需要一个公共成员函数来实现这一点。尽管这可以由实例成员函数完成,但通常我们为此使用静态成员函数(static memberfunction)。
静态成员函数可以通过对象访问静态数据成员,也可以在不存在对象时通过类的名称访问静态数据成员。换言之,使用静态成员函数,我们可以在实例想要访问相应的静态数据成员时访问它,或者在应用程序需要访问相应的静态数据成员时访问它。
注意,静态成员函数没有宿主对象,因为静态成员函数不与任何实例关联。
1. 声明静态成员函数
静态成员函数和静态数据成员一样属于类。静态成员函数应该在类中声明,但必须用关键字 static 限定。
2. 定义静态成员函数
与实例成员函数相类似,静态成员函数必须在类外部定义。静态成员函数和实例成员函数的定义没有区别。如果我们想查看函数定义是实例函数还是静态函数,我们需要参考其声明。
注意,我们不能使用const 限定符,因为没有宿主对象。
3. 调用静态成员函数
静态成员函数可以通过实例或者类调用。要通过实例调用静态成员函数,我们使用与调用实例成员函数相同的语法; 要通过类调用静态成员函数,我们使用类的名称和类解析运算符(::)。
警告:不能使用静态成员函数访问实例数据成员,因为静态成员函数没有隐藏的 this指针。该指针定义了需要引用的实例。
在另一方面,实例成员函数则可以访问静态数据成员(不使用this 指针),但我们通常避免这样做。最佳编程实践方法是使用实例成员函数访问实例数据成员,使用静态成员函数访问静态数据成员。我们建议程序中实例成员的区域与静态成员的区域以符号方式分离。
五、初始化问题
Initialization of a variable provides its initial value at the time of construction.
初始化是在变量构建之时提供其初始值
之前,提到过不少初始化,当引入类之后,初始化会更加复杂一点,尤其是后面的对象初始化往往有不同的形式,所以在这里介绍一下C++中的初始化问题。
The initial value may be provided in the initializer section of a declarator or a new expression. It also takes place during function calls: function parameters and the function return values are also initialized.
初始值可能在声明符的初始化部分或new表达式中提供。并且在函数调用期间也会进行初始化操作,包括函数参数和函数返回值。
对于每个声明符,其初始化器可能是以下之一:
按照其初始化实现机制,有以下划分
(一)默认初始化
当声明变量但未提供初始化器时,默认初始化规则将被应用。
基本形式
1、类类型的默认初始化
if T is a [non-POD (until C++11) ]class type, the constructors are considered and subjected to overload resolution against the empty argument list. The constructor selected (which is one of the default constructors) is called to provide the initial value for the new object;
如果 T 是一个[非 POD ,直到 C++11 ]类类型,那么会考虑其构造函数,并针对空参数列表进行重载解析。所选择的构造函数(这是默认构造函数之一)会被调用,为新对象提供初始值。
注:POD(plain old data)是形同C结构体/联合体简单的数据结构
1、平凡(Trivial),不具有非默认的特殊函数;
2、标准的布局(Standard-Layout),所有非静态数据成员都拥有相同访问权限并且类中至多有一个基类(如果有基类,则该基类也必须是标准布局的),同时满足其他一些条件的类或结构体
3、所有非静态数据成员都是POD类型。
如果类类型不存在无参构造,则无法进行默认初始化
在函数体之外定义的类变量,如果类具有默认构造函数,会先进行零初始化,然后执行默认构造函数。
一些IDE可能会优化,但是理论上是不确定值
2、数组的默认初始化
if T is an array type, every element of the array is default-initialized;
如果T是数组类型,则每一个元素都将进行默认初始化
当数组定义时提供了初始值列表,对于未定义的元素,如果是内置类型或者有合成的默认构造函数,则会先进行零初始化。
若元素是类类型,则会执行默认构造函数。当数组定义时未提供初始化列表,每个元素都将执行默认初始化。
3、内置类型的默认初始化
内置类型的默认初始化情况取决于其定义的位置。在函数体之外(全局变量)定义的内置类型变量,会被默认初始化为 0 ,在函数体内部(局部变量)定义的内置类型变量,其默认初始值是未定义的,即一个随机值。
(二)值初始化
This is the initialization performed when a variable is constructed with an empty initializer.
当使用空初始化器构造变量时,将执行此初始化。
1、类类型
如果T是一个没有默认构造函数,或者有一个用户提供的或已删除的默认构造函数的类类型,则对象将被默认初始化;
如果T是一个具有默认构造函数的类类型,该默认构造函数既不是用户提供的也不是已删除的(即,它可能是一个具有隐式定义的或默认的默认构造函数的类),则该对象将被零初始化,然后如果它有一个非平凡的默认构造函数,则将其默认初始化。
平凡的构造函数是指默认生成的构造函数,或者用户提供的但是等效于默认的构造函数(没有额外操作)
对于1、2、5、6
In all cases, if the empty pair of braces {} is used and T is an aggregate type, aggregate-initialization is performed instead of value-initialization.
在所有情况下,如果使用空大括号{}并且T是聚合类型,则执行聚合初始化而不是值初始化。
2、数组
if T is an array type, each element of the array is value-initialized;
如果T是数组类型,则数组的每个元素都进行值初始化;
int a[3]{}; // 栈空间
new int[4]{}; // 堆空间
除了类类型和数组,其他类型,内置基本类型,值初始化意味着都将进行零初始化
otherwise, the object is zero-initialized.
否则,对象将零初始化
4、值初始化vs默认初始化
相同点:值初始化和默认初始化都是在变量或对象创建时赋予初始值的方式。
不同点:
- 触发条件不同:默认初始化是在未指定初始值时发生,而值初始化可以通过特定的表达式显式触发。
- 初始值不同:内置类型在默认初始化时,全局变量初始化为 0 ,局部变量初始值未定义;而值初始化时,内置类型通常初始化为 0 。
- 对于类类型,默认初始化取决于类是否有默认构造函数以及成员变量的情况,而值初始化在类有默认构造函数时会调用它。
(三)直接初始化
Initializes an object from explicit set of constructor arguments.
通过显示的构造参数集合初始化一个对象
1、变量初始化
一般形式
The array is initialized as in aggregate initialization, except that narrowing conversions are allowed and any elements without an initializer are value-initialized.
如果T是数组类型,在C++20之前,使用圆括号,程序形式不良(ill-formed),在C++20后,这种初始化方式类似于聚合初始化(aggregate initialization),但允许缩小转换(narrowing conversions),并且任何没有初始化器的元素都将被值初始化。
如果T是类类型,对于聚合类 ,初始化方式和C相似但是更加严格,这时候可以把类类型相当C的结构体
对于非聚合类,就需要将其看作是类
圆括号大多数情况下都是调用的意思,所以此时类类型一般要求是非聚合类
但是 C++ 20开始允许使用圆括号初始化非聚合类,就像是初始化数组,这也算是和花括号同步了
if the destination type is a (possibly cv-qualified) aggregate class, it is initialized as described in aggregate initialization except that narrowing conversions are permitted, designated initializers are not allowed, a temporary bound to a reference does not have its lifetime extended, there is no brace elision, and any elements without an initializer are value-initialized.
如果目标类型是一个(可能是cv限定的)聚合类,它将按照聚合初始化中所描述的方式进行初始化,除了允许窄化转换、不允许指定初始化器、临时绑定到引用不会延长其生存期、没有大括号省略、没有初始化器的任何元素都将进行值初始化。
对于其它基本类型
2、无名对象初始化
一般形式
对于基本类型,这类似与类型转换
1、2、4将初始化一个纯右值临时对象,一般用于赋值给其他对象
int a = int(1); // 在C语言语境中,这种相当于int a = (int)1;
int b = float(1); // 强制类型转换
int c = int(a); // 强制类型转换
int d = static_cast<int>(1.5); // 强制类型转换
int * e = new int(1);
对于类类型,如果目标类型是一个聚合类(aggregate class),那么它将按照聚合初始化的方式进行初始化,但有一些不同点:
- 允许缩小转换(narrowing conversions)。
- 不允许指定初始化器(designated initializers)。
- 绑定到引用的临时对象的生命周期不会延长。
- 不会发生大括号省略(brace elision)。
- 任何没有初始化器的元素都将被值初始化。
struct MyStruct {
int x;
int y;
};
// C++ 20前不允许
MyStruct(1,2);
MyStruct(1, 2.f); // 窄化转换
MyStruct(1);
如果不是聚合类,则会调用相应的构造函数。特殊的如果初始化器是一个prvalue(纯右值)表达式,并且它的类型与T相同(忽略cv限定符),那么初始化器表达式本身将用于初始化目标对象,而不是从它创建的一个临时对象。这个过程称为copy elision(复制省略),自C++17起被引入。
struct MyStruct {
int x;
int y;
MyStruct() {
std::cout << "Default constructor called" << std::endl;
}
MyStruct(int a, int b) : x(a), y(b) {
std::cout << "Struct constructor called" << std::endl;
}
MyStruct(const MyStruct& other) {
x = other.x;
y = other.y;
std::cout << "Copy constructor called" << std::endl;
}
void print() {
std::cout << "X: " << x << ", Y: " << y << std::endl;
}
};
// 值初始化,x和y默认初始化
MyStruct().print() ;
MyStruct(MyStruct()).print() ; // 从一个纯右值对象上直接构建,注意,不是上面那个
std::cout << "-----------------" << std::endl;
// 直接初始化
MyStruct(1,2).print() ;
MyStruct(MyStruct(1,2)).print() ;// 从一个纯右值对象上直接构建,注意,不是上面那个
std::cout << "-----------------" << std::endl;
MyStruct s1 = MyStruct();
s1.print() ;
MyStruct(s1).print() ;// 调用拷贝构造
3、类成员初始化
,
构造器初始化列表,看起来像是特殊形式的直接初始化
特殊的,可以调用其它构造函数,这种构造叫做委托构造,被调用的构造叫做目标构造
这时候,列表必须仅由该一个成员初始化项组成
并且注意不能有递归构造
(四)拷贝初始化
Initializes an object from another object
从另一个对象初始化对象
1、拷贝初始化的场景
拷贝初始化常见于以下情况:
1. 当一个非引用类型的命名变量(自动变量、静态变量或线程局部变量)在声明时使用等于号后跟一个表达式的初始化器进行初始化时。
int a = 10;
2. 当按值传递函数参数时。
add(1,2);
3. 当从按值返回的函数返回时。
int a = add(1,2);
2、显式构造与隐式构造
当T与other具有显著不同的数据类型时,例如T是类类型,而other不是,则,转换构造随之发生
如果T是一个类类型,且other的类型(cv-unqualified版本)不是T或从T派生的类,或者如果T是非类类型,但other的类型是类类型,则检查可以将other的类型转换为T的用户定义转换序列,并通过重载决议选择最佳的一个。然后,使用转换的结果(如果使用了转换构造函数,则为prvalue表达式(C++17起))直接初始化对象。
· 这种构造也可称为 隐式构造
A constructor that can be used to perform an implicit conversion is called a converting constructor. By default, all constructors are converting constructors.
可用于执行隐式转换的构造函数称为转换构造函数。默认情况下,所有构造函数都是转换构造函数。
隐式构造可能会在有些方面好用,但是有时,为了安全考虑(避免在某个过程中,由于编译器自动的隐式转换而造成意料之外的结果),我们需要显式构造,使用关键字explicit
显式的声明,会让程序更具有稳定性。
注:
显式构造函数不能用于拷贝初始化或拷贝列表初始化。
显式构造函数不能用于隐式转换(因为它使用拷贝初始化或拷贝列表初始化)。
3、拷贝初始化vs直接初始化
在调用构造函数方面,拷贝初始化首先使用指定构造函数创建一个临时对象,然后用复制构造函数将那个临时对象复制到正在创建的对象。
而直接初始化直接调用与实参匹配的构造函数。例如,对于类MyClass,MyClass obj1("param"); 是直接初始化,直接调用匹配的构造函数。MyClass obj2 = "param"; 是拷贝初始化,先创建临时对象再复制。
4、拷贝初始化vs赋值
拷贝初始化是在创建变量时赋予其一个初始值,而赋值是把对象当前值擦除,以一个新值代替。例如,对于整型变量,int i = 0; 是拷贝初始化,i = 6; 是赋值。对于类对象,拷贝初始化如string s = "hello"; ,赋值如string s2; s2 = "world"; 。拷贝初始化在创建对象时进行,而赋值是在对象已存在的情况下修改其值。
(五)列表初始化
Initializes an object from braced-init-list
从花括号初始化列表中初始化一个对象。
从某种意义上,前四种初始化方式已经足够容括所有赋值行为,简单的说,
默认初始化->不确定值
值初始化->默认值
拷贝初始化->临时对象装载确定值,拷贝给新对象
直接初始化->以确定值创建对象
所以从列表初始化开始,更多的是初始化的目标的差异,列表初始化的所有情况几乎都在前面涉及到了,现在总结一下。
C++中的列表初始化(List Initialization)是一种使用花括号 {} 来初始化对象的方法。这种方法在C++11中被引入,并在后续版本中得到了增强和改进。列表初始化可以用于多种情况,包括变量初始化、函数参数传递、返回值、构造函数调用等。
1、初始化方式
列表初始化可以分为两种:
直接列表初始化(Direct-list-initialization):
使用 T object { ... } 的形式。
在初始化一个命名变量、一个未命名的临时对象、一个动态存储对象、非静态数据成员(不使用等号),以及构造函数成员初始化列表时使用。
复制列表初始化(Copy-list-initialization):
使用 T object = { ... } 的形式。
在初始化一个命名变量(使用等号)、函数调用参数、返回值、赋值表达式、构造函数调用等情况下使用。
2、初始化效果
列表初始化的效果取决于被初始化对象的类型,特别的:
对于类类型,不外乎,
聚合类型(Aggregate type):后面介绍,进行聚合初始化
非聚合类型:会调用构造,这涉及到很多东西,这里不说明
数组类型
对于简单的数组,这很容易理解和使用,但是对于
字符数组:如果初始化列表中只有一个适当类型的字符串字面量,则数组从该字符串字面量初始化。后面介绍
还有一些特殊的类型和情况,遇到再讨论。
3、窄化转换限制
列表初始化禁止以下几种窄化转换,已经说明过:
- 从浮点类型到整数类型的转换。
- 从 long double 到 double 或 float 的转换,以及从 double 到 float 的转换,除非源表达式是常量表达式且不会发生溢出。
- 从整数类型到浮点类型的转换,除非源表达式是常量表达式且其值可以精确存储在目标类型中。
- 从整数或未作用域枚举类型到无法表示原类型所有值的整数类型的转换,除非源表达式是常量表达式且其值可以精确存储在目标类型中。
(六)聚合初始化
Initializes an aggregate from braced-init-list。Aggregate initialization initializes aggregates. It is a form of list-initialization (since C++11) or direct initialization (since C++20)
从带括号的init-list初始化聚合。聚合初始化初始化聚合。它是列表初始化(c++ 11起)或直接初始化(c++ 20起)的一种形式。
聚合类型就其定义
- 数组类型。
- 类类型(通常是
struct
或union
),需满足以下条件:
- 无私有或受保护的直接非静态数据成员(自C++17起)。
- 无用户提供的构造函数(显式默认或删除的构造函数允许,自C++20起)。
- 无虚拟、私有或受保护的基类(自C++17起)。
- 无虚拟成员函数。
- 无默认成员初始化器(自C++11起)。
可以简单的认为,就是一个C的结构或联合
In general programming, an aggregate data type (also called an aggregate) is any type that can contain multiple data members. Some types of aggregates allow members to have different types (e.g. structs), while others require that all members must be of a single type (e.g. arrays).
在一般编程中,聚合数据类型(也称为聚合)是可以包含多个数据成员的任何类型。某些类型的聚合允许成员具有不同的类型(例如结构),而其他类型则要求所有成员必须具有单一类型(例如数组)。
In C++, the definition of an aggregate is narrower and quite a bit more complicated.
在c++中,聚合的定义更窄,也更复杂。
The key thing to understand at this point is that structs with only data members are aggregates.
这里要理解的关键是,只有数据成员的结构是聚合。
尽管,和C语言的结构体很类似,但是,C++的聚合初始化语法要比C严格得多,这点,在前面已经提到过
初始化效果:
- 按顺序初始化数组元素或类成员,允许隐式转换但禁止窄化转换。
- 如果初始化列表的条款少于成员数量,剩余成员将进行值初始化或使用默认成员初始化器。
- 联合类型仅初始化第一个非静态数据成员。
(七)引用初始化
C++中的引用初始化是一种机制,通过它,一个引用变量被绑定到一个特定的对象上。引用一旦初始化后,就不能再指向其他对象。在之前介绍过引用,但是不得不说,这里面仍然有不少复杂性的方面,这里仅仅是和初始化一同引入。
C++中的引用初始化可以通过以下几种方式进行:
简单初始化:
T& ref = object; // ref绑定到object
使用花括号初始化:
T& ref = {arg1, arg2, ...};
函数调用初始化:
T& ref = fn(); // 假设fn()返回类型为T
构造函数初始化列表:
class MyClass {
public:
MyClass(T& ref) : m_ref(ref) {} // 在构造函数初始化列表中初始化引用成员
private:
T& m_ref;
};