
往期《C++初阶》回顾:/------------ 入门基础 ------------/ 【C++的前世今生】 【命名空间 + 输入&输出 + 缺省参数 + 函数重载】 【普通引用 + 常量引用 + 内联函数 + nullptr】 /------------ 类和对象 ------------/ 【类 + 类域 + 访问限定符 + 对象的大小 + this指针】 【类的六大默认成员函数】
哈喽呀小伙伴们~一周不见想死你们啦!(づ。◕‿‿◕。)づ 上周有没有乖乖学习呀?🤔 至于博主我嘛👉 刚忙完期末复习大作战💻 ,博主可是被期末复习按在地上摩擦了一周呢 (╯°□°)╯︵ ┻━┻
不过现在终于忙完啦!虽然还有一周才考试…但是!咱这闲不住的人哪能真闲着呀✨ 所以!今天就马不停蹄给大家带来《C++ 初阶之类和对象》【下】的第一篇宝藏博客啦🎁(๑•̀ㅂ•́)و✧ 据说看完这篇的小伙伴都直呼"原来C++还能这样玩?"(≧∇≦)ノ 快搬好小板凳准备上课啦~
初始化列表(Initializer List):是 C++ 中用于在对象构造时直接初始化成员变量的语法。
:开始,后面跟着用逗号分隔的成员变量初始化列表。
class MyClass
{
int _a;
double _b;
std::string _s;
public:
// 初始化列表语法
MyClass(int a, double b, const std::string& s)
: _a(a), _b(b), _s(s) // 初始化列表
{
// 构造函数体(此时成员已初始化)
}
};使用初始化列表主要有以下两点原因:
--------------------为了提高效率--------------------
对于类类型的成员变量 :
进行初始化,然后在构造函数体中再进行赋值操作,这会产生额外的开销。进行初始化,避免了先默认构造再赋值的过程,从而提高了初始化效率。 string类型时,使用初始化列表可以直接调用string的构造函数来初始化,而不是先默认构造一个空字符串,再进行赋值。class Student
{
string _name;
public:
// 低效写法:先默认构造空字符串,再赋值
Student(const std::string& name)
{
_name = name;
}
// 高效写法:直接调用string的拷贝构造
Student(const string& name) : _name(name) {}
};--------------------不得不使用--------------------
初始化 const 成员和引用成员:
const成员变量和引用成员在定义后就不能被修改,因此必须在定义时进行初始化。
const成员和引用成员的唯一方法。如果不使用初始化列表,试图在构造函数体中对const成员或引用成员进行赋值,将会导致编译错误。
----------------------------初始化 const 成员----------------------------
class ConstDemo
{
const int _value;
public:
ConstDemo(int v) : _value(v) {} // 必须用初始化列表
};
----------------------------初始化 const 成员----------------------------
class RefDemo
{
int& _ref;
public:
RefDemo(int& r) : _ref(r) {} // 必须用初始化列表
}; 调用无默认构造的成员:
class Engine
{
public:
Engine(int power) {} // 只有带参构造
};
class Car
{
Engine _engine;
public:
Car() : _engine(100) {} // 必须显式初始化
};#include<iostream>
using namespace std;
// Time类定义
class Time
{
public:
/*---------------Time类的构造函数(带参数)---------------*/
// 注意:这个类没有提供默认构造函数(无参构造函数)
Time(int hour)
:_hour(hour)
{
cout << "Time类的构造函数Time()" << endl;
}
private:
int _hour;
};
// Date类定义
class Date
{
public:
/*---------------Date类的构造函数---------------*/
Date(int& x, int year = 1, int month = 1, int day = 1)
:_year(year) // 初始化_year
, _month(month) // 初始化_month
, _day(day) // 初始化_day
, _t(12) // 初始化Time类成员(必须提供参数)
, _ref(x) // 初始化引用成员(必须通过初始化列表)
, _n(1) // 初始化const成员(必须通过初始化列表)
{
// 如果尝试在构造函数体内初始化以下成员,会导致编译错误:
// 1. _t(12) - 错误:Time类没有默认构造函数
// 2. _ref = x - 错误:引用必须在初始化列表中初始化
// 3. _n = 1 - 错误:const成员必须在初始化列表中初始化
}
/*---------------打印日期信息---------------*/
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl; //const成员函数,保证不会修改对象状态
}
private:
int _year;
int _month;
int _day;
Time _t; // Time类成员(没有默认构造函数)
int& _ref; // 引用成员 (必须在初始化列表中初始化)
const int _n; // const成员 (必须在初始化列表中初始化)
};
int main()
{
int i = 0; //定义一个整型变量用于初始化Date的引用成员
// 创建Date对象d1
// 参数:
// i - 用于初始化_ref引用成员
// 其他参数使用默认值
Date d1(i);
// 调用Print方法输出日期信息
d1.Print();
return 0;
}
/*
* 关键点总结:
* 1. 初始化列表的必要性:
* - 必须用于初始化:引用成员(_ref)、const成员(_n)、没有默认构造函数的类成员(_t)
*
* 2. 初始化顺序:
* - 成员变量的初始化顺序由它们在类中的声明顺序决定(_year→_month→_day→_t→_ref→_n)
* - 与初始化列表中的书写顺序无关
*
* 3. 特殊成员初始化:
* - 引用和const成员:只能在初始化列表中初始化
* - 没有默认构造的类成员:必须在初始化列表中显式构造
*
* 4. 良好实践:
* - 即使对普通成员变量(如:_year)也使用初始化列表
* - 保持初始化列表顺序与成员声明顺序一致
*/
特性 | 初始化列表 | 构造函数内赋值 |
|---|---|---|
const成员 | ✔️ 支持 | ❌ 编译错误 |
引用成员 | ✔️ 支持 | ❌ 编译错误 |
无默认构造的成员 | ✔️ 支持 | ❌ 编译错误 |
性能 | 更高效(直接构造) | 可能低效(先默认构造再赋值) |
初始化顺序 | 由成员声明顺序决定 | 无顺序依赖 |
在 C++ 中,成员变量的初始化遵循两大规则:
其一:初始化顺序仅取决于变量在类中声明的先后次序,与初始化列表中的排列顺序无关。
class Order
{
int a; // 先声明
int b;
public:
Order() : b(1), a(2) {} // 实际初始化顺序:a→b
};其二:每个成员变量在初始化列表中只能出现一次,作为其完成定义与初始化的关键位置,确保对象创建时各成员获得唯一且正确的初始状态。
下面是一道关于:初始化顺序的自测题(看完代码不要看下面的注释分析,因为那是答案)😄
#include <iostream>
using namespace std;
class A
{
public:
/*---------------构造函数---------------*/
/**
* 初始化列表说明:
* 1. _a1初始化为参数a的值
* 2. _a2初始化为_a1的值(此时_a1尚未初始化,存在风险)
*
* 注意:成员初始化顺序由声明顺序决定,与初始化列表顺序无关
*/
A(int a)
:_a1(a) // 初始化_a1为传入的参数a
, _a2(_a1) // 用_a1的值初始化_a2(此时_a1尚未初始化)
{
}
/*---------------打印成员变量值---------------*/
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
private:
// 注意成员变量的声明顺序:_a2在前,_a1在后
// 这将影响初始化顺序,即使初始化列表中的顺序不同
int _a2 = 2; // 成员_a2,有类内初始值2(但会被初始化列表覆盖)
int _a1 = 2; // 成员_a1,有类内初始值2(但会被初始化列表覆盖)
};
int main()
{
// 创建A类对象,传入参数1
A aa(1);
// 打印对象成员值
aa.Print();
return 0;
}
-------------------------------我下面是:分析和答案----------------------------
/*
* 关键点分析:
* 1. 成员初始化顺序:
* - 严格按照类中声明的顺序进行(先_a2,后_a1)
* - 与初始化列表中的书写顺序无关
*
* 2. 初始化过程:
* a) 首先尝试初始化_a2:
* - 使用_a1的值(此时_a1尚未初始化,值不确定)
* - 类内初始值2被初始化列表覆盖
* b) 然后初始化_a1:
* - 使用构造函数参数1
* c) 最后执行构造函数体(本例为空)
*
* 4. 类内初始值的作用:
* - 如果初始化列表不显式初始化成员,则使用类内初始值
* - 本例中初始化列表覆盖了类内初始值
*/
类成员变量可以在声明时直接指定初始值(缺省值),缺省值会在构造函数调用前生效,这些值会在对象构造时被使用,除非初始化列表中显式覆盖它们。
#include<iostream>
using namespace std;
// Time类定义
class Time
{
public:
/*---------------Time类构造函数---------------*/
Time(int hour)
:_hour(hour)
{
cout << "Time类构造函数Time(int hour)调用" << endl;
}
private:
int _hour;
};
// Date类定义
class Date
{
public:
/*---------------Date类默认构造函数---------------*/
Date()
:_month(2) // 显式初始化_month为2(会覆盖缺省值1)
{
// 构造函数体内可以执行其他操作
cout << "Date类默认构造函数Date()调用" << endl;
/*
* 注意:以下成员已在类定义中提供了缺省值:
* _year = 1
* _day 未显式初始化(内置类型未初始化时为随机值)
* _t = 1 (调用Time(int)构造函数)
* _n = 1 (const成员)
* _ptr = (int*)malloc(12) (动态分配内存)
*/
}
/*---------------打印日期信息---------------*/
void Print() const
{
// 注意:_day未初始化,打印的值是未定义的
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
// 成员变量声明与缺省值初始化(C++11特性)
// 内置类型缺省值(如果初始化列表不显式初始化则使用此值)
int _year = 1; // 年份,缺省值为1
int _month = 1; // 月份,缺省值为1(但构造函数初始化列表会覆盖为2)
int _day; // 日期,没有缺省值(未初始化)
// 类类型成员缺省初始化
Time _t = 1; // 调用Time(int)构造函数初始化
// const成员缺省值
const int _n = 1; // const成员必须在声明时或初始化列表中初始化
// 指针成员动态内存分配
int* _ptr = (int*)malloc(12); // 使用malloc分配12字节内存
};
int main()
{
// 创建Date对象d1
Date d1; //调用默认构造函数
// 打印日期信息
d1.Print(); //输出格式:year-month-day
return 0;
}
/*
* 关键点总结:
* 1. 缺省成员初始化(C++11特性):
* - 在类声明中直接为成员变量指定初始值
* - 如果构造函数初始化列表没有显式初始化,则使用缺省值(初始化列表的优先级高于缺省值)
* - 初始化列表的初始化会覆盖缺省值(如:_month被初始化为2而非1)
*
* 2. 成员初始化顺序:
* - 首先按照缺省值初始化
* - 然后执行构造函数初始化列表的初始化
* - 最后执行构造函数体内的代码
*
* 3. 特殊成员处理:
* - _day:没有缺省值,未在初始化列表中初始化,值是未定义的
* - _t:类类型成员,使用Time(int)构造函数初始化
* - _n:const成员,必须在声明时或初始化列表中初始化
* - _ptr:使用malloc动态分配内存
*/
初始化列表的特点:
先于构造函数体:无论成员变量是否在初始化列表中显式列出,其初始化都在构造函数体执行 之前 完成隐式初始化:未在初始化列表中显式初始化的成员变量,仍会按默认规则初始化(如:默认构造函数、缺省值或未定义值)在 C++ 中,推荐优先使用初始化列表对成员变量进行初始化, 原因如下:即使成员变量未在初始化列表中显式初始化,其初始化过程依然会经过初始化列表阶段。
若成员变量在声明时指定了默认值,初始化列表将直接采用该默认值进行初始化。 若未指定默认值:
对于内置类型成员,其是否初始化由编译器决定,C++ 标准未作强制规定,这可能导致变量处于未初始化的不确定状态。对于自定义类型成员: 默认构造函数 进行初始化。构造函数,而非默认构造函数。#include <iostream>
using namespace std;
class MyClass
{
public:
MyClass()
{
cout << "默认构造函数被调用" << endl;
}
MyClass(int value)
{
cout << "带参构造函数被调用,参数:" << value << endl;
}
};
// 案例1:未显式初始化(调用默认构造函数)
class Container1
{
private:
MyClass obj1;
public:
Container1() //未在初始化列表中显式初始化
{
cout << "Container1构造函数体执行" << endl;
}
};
// 案例2:显式初始化(调用带参构造函数)
class Container2
{
private:
MyClass obj2;
public:
Container2() : obj2(42) //显式初始化
{
cout << "Container2构造函数体执行" << endl;
}
};
int main()
{
cout << "==== 案例1 ====" << endl;
Container1 c1;
cout << "\n==== 案例2 ====" << endl;
Container2 c2;
return 0;
}
自定义类型转换的种类:
内置类型 → 类类型:
当类定义了单参数构造函数(或:除第一个参数外其余参数均有默认值)时,C++ 允许将该参数类型的值隐式转换为类对象。
示例:
class MyClass
{
public:
MyClass(int value) { /* ... */ } // 单参数构造函数
};
void func(MyClass obj);
func(42); // 隐式转换:int → MyClass类类型 → 类类型:
若类 A 定义了以类 B 为参数的构造函数,则类 B 的对象可隐式转换为类 A 的对象。
示例:
class B {};
class A
{
public:
A(const B& b) { /* ... */ } // 以B为参数的构造函数
};
void func(A obj);
B b;
func(b); // 隐式转换:B → A注意:上面的这两种类型转换都是隐式类型转换
代码小案例:展示使用自定义类型的类型转换
#include<iostream>
using namespace std;
class A
{
public:
/*----------------------单参数构造函数----------------------*/
/**
* 注意:如果加上explicit关键字,将禁止隐式类型转换
* 例如:A aa1 = 1; 这样的语句将无法编译
*/
A(int a1)
:_a1(a1)
{
}
/*----------------------双参数构造函数----------------------*/
/**
* 注意:如果加上explicit关键字,将禁止隐式类型转换
* 例如:A aa3 = {2,2}; 这样的语句将无法编译
*/
A(int a1, int a2)
:_a1(a1), _a2(a2)
{
}
/*----------------------打印成员变量值----------------------*/
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
int Get() const
{
return _a1 + _a2;
}
private:
int _a1 = 1; // 成员变量_a1,默认值为1
int _a2 = 2; // 成员变量_a2,默认值为2
};
class B
{
public:
/*----------------------构造函数,接收A类型的引用----------------------*/
/**
* 这个构造函数允许从A类型到B类型的隐式转换
*/
B(const A& a)
:_b(a.Get()) // 使用A对象的Get()方法初始化_b
{
}
private:
int _b = 0; // 成员变量_b,默认值为0
};
int main()
{
// 1. 隐式类型转换示例1:int → A
// 编译器实际执行:
// 1) 先用1构造一个临时A对象
// 2) 再用这个临时对象拷贝构造aa1
// 3) 编译器优化为直接构造
A aa1 = 1; // 等价于 A aa1(1);
aa1.Print(); // 输出:1 2
// 2. 隐式类型转换示例2:int → A → const A&
// 用1构造临时A对象,然后绑定到引用aa2
const A& aa2 = 1; // 临时对象生命周期延长至引用作用域结束
// 3. C++11列表初始化(多参数隐式转换)
// 使用初始化列表构造A对象
A aa3 = { 2,2 }; // 等价于 A aa3(2,2);
aa3.Print(); // 输出:2 2
// 4. 隐式类型转换示例3:A → B
// 通过A对象隐式构造B对象
// 实际过程:
// 1) 调用A::Get()获取值
// 2) 用该值构造B对象
B b = aa3; // 等价于 B b(aa3);
// 5. 隐式类型转换示例4:A → B → const B&
// 通过A对象隐式构造临时B对象,然后绑定到引用
const B& rb = aa3;
return 0;
}
/*
* 关键点总结:
* 1. 隐式类型转换规则:
* - 当类有单参数构造函数时,支持从参数类型到类类型的隐式转换
* - C++11开始支持多参数构造函数的隐式转换(使用初始化列表语法)
*
* 2. explicit关键字的作用:
* - 修饰构造函数时,禁止隐式类型转换
* - 需要显式调用构造函数(如 A aa1(1) 而不是 A aa1 = 1)
*
* 3. 引用绑定临时对象:
* - const引用可以绑定到隐式转换产生的临时对象
* - 临时对象的生命周期会延长至引用的作用域结束
*/
explicit 关键字:是 C++ 中用于修饰 构造函数 或 类型转换运算符 的关键字。
示例:
class MyClass
{
public:
explicit MyClass(int value) { /* ... */ } // 禁止隐式转换
};
void func(MyClass obj);
func(42); // 错误:无法隐式转换
func(MyClass(42)); // 正确:显式构造在没有 explicit 的情况下,C++ 允许 隐式转换,这可能导致意外的行为。
class MyString
{
public:
MyString(const char* str) { /* 构造函数 */ }
void print() { cout << str; }
private:
string str;
};
void func(MyString s)
{
s.print();
}
int main()
{
func("Hello"); // 隐式转换:const char* → MyString
return 0;
}问题:
func("Hello") 会自动调用 MyString(const char*) 构造函数,但可能并非程序员本意。MyString 有多个构造函数,隐式转换可能导致歧义。修饰构造函数:禁止编译器进行隐式构造,必须显式调用。
class MyString
{
public:
explicit MyString(const char* str) { /* ... */ } // 禁止隐式转换
};
void func(MyString s)
{
s.print();
}
int main()
{
// func("Hello"); // ❌ 错误:不能隐式转换
func(MyString("Hello")); // ✅ 必须显式构造
return 0;
}修饰类型转换运算符:禁止隐式类型转换,必须显式转换。
class MyInt
{
public:
explicit operator int() const //禁止隐式转换
{
return value;
}
private:
int value;
};
int main()
{
MyInt num;
// int x = num; // ❌ 错误:不能隐式转换
int x = static_cast<int>(num); // ✅ 必须显式转换
return 0;
}
static成员:是类的特殊成员,它们 不属于任何一个类对象,而是 属于类本身,被所有类对象共享。
static 成员分为两种:
static成员变量:用static修饰的成员变量。
定义与特性:
static成员变量只有一份实例,被所有对象共享。内存分配与初始化:
它在程序启动时分配内存。
它必须在类外进行初始化。(即使有默认值也需在类外显式初始化)
class MyClass
{
public:
static int sharedVar; //声明静态成员变量
};
int MyClass::sharedVar = 10; //在类外初始化访问方式:
::直接访问。 MyClass obj; int value1 = MyClass::sharedVar; int value2 = obj.sharedVar; ,但推荐用类名访问,更能体现其静态属性。作用:常用于统计类对象的数量,或者存储类的全局相关信息。
1. 存储特性
全局唯一性:无论创建多少个类的对象,static成员变量在内存中仅有一份实例,被所有对象共享。静态存储区:存储在程序的静态存储区,生命周期从程序启动到结束,不依赖于对象的创建与销毁。2. 初始化规则
类外显式初始化:必须在类外进行初始化。不支持类内默认值:C++ 标准禁止在类内直接为static成员变量设置缺省值。(除非是constexpr类型)3. 访问特性
类名直接访问:可通过类名::静态成员名访问,无需创建对象。受访问限定符约束:与普通成员变量相同,受public/protected/private限制。4. 功能特性
类级别数据:用于存储与类相关的全局信息(如:对象计数、配置参数)独立于对象:不与任何对象绑定,可在无对象实例时使用。可被const成员函数修改:static成员变量可在const成员函数中被修改,因其不属于任何对象实例。static 成员函数:用static修饰的成员函数。
定义与特性:
同样属于类本身,它不依赖于类的具体对象,没有隐含的this指针(不能访问非静态成员变量和非静态成员函数 )
class MyClass
{
public:
static void staticFunc()
{
// 这里只能访问静态成员变量,不能访问非静态成员
// 如不能访问非静态成员变量:nonStaticVar
}
static int sharedVar;
private:
int nonStaticVar;
};
int MyClass::sharedVar = 0;调用方式:
MyClass::staticFunc();用途:通常用于实现一些与类整体相关,而不涉及具体对象状态的功能。
1. 归属特性
属于类而非对象:static成员函数不绑定到任何对象实例,即使没有创建类的对象也可调用。无this指针:无法使用this指针,因此不能访问非静态成员变量或调用非静态成员函数。2. 调用方式
类名直接调用:通过类名::静态函数名()调用,无需创建对象。对象调用允许但不推荐:也可通过对象实例调用(如:obj.staticFunc()),但语义上不直观。3. 访问权限
仅访问静态成员:只能访问类的静态成员变量和静态成员函数,无法访问非静态成员(包括普通成员变量和非静态成员函数)。受访问限定符约束:与普通成员函数相同,受public/protected/private限制。4. 功能特性
工具性方法:常用于实现与类相关但不依赖对象状态的工具函数(如:工厂方法、配置管理)全局接口封装:可作为类的全局接口,隐藏类的实现细节(如:单例模式中的getInstance()方法)5. 特殊规则
不能声明为const:由于没有this指针,static成员函数不能被声明为const不能是虚函数:static成员函数不参与多态,无法被声明为virtual可在类内定义:可直接在类内实现,也可在类外实现(需指定类名::作用域)代码案例:使用静态成员变量和静态成员函数
#include<iostream>
using namespace std;
class A
{
public:
/*----------------------默认构造函数----------------------*/
A()
{
++_scount; // 每创建一个对象时,静态计数器_scount加1
}
/*----------------------拷贝构造函数----------------------*/
A(const A& t)
{
++_scount; // 每拷贝构造一个对象时,静态计数器_scount加1
}
/*----------------------析构函数----------------------*/
~A()
{
--_scount; // 对象销毁时,静态计数器_scount减1
}
/*----------------------静态成员函数----------------------*/
/**
* @brief 静态成员函数:获取当前对象数量
* @return 当前存在的对象数量
*
* 注意:
* 1. 静态成员函数可以直接通过类名调用
* 2. 静态成员函数只能访问静态成员变量
*/
static int GetACount()
{
return _scount;
}
private:
// 静态成员变量声明(类内)
static int _scount; //用于记录当前存在的A类对象数量
};
// 静态成员变量定义和初始化(类外)
// 必须单独在类外进行定义和初始化
int A::_scount = 0;
int main()
{
// 测试1:初始对象数量
cout << A::GetACount() << endl; // 输出:0(尚未创建任何对象)
// 测试2:创建对象后的数量
A a1, a2; // 调用两次默认构造函数(_scount = 2)
A a3(a1); // 调用拷贝构造函数(_scount = 3)
cout << A::GetACount() << endl; // 输出:3(三个对象存在)
// 测试3:通过对象调用静态成员函数
// 语法允许但不推荐,容易造成误解
cout << a1.GetACount() << endl; // 输出:3(与类名调用结果相同)
// 测试4:尝试直接访问私有静态成员(编译错误)
// 错误原因:_scount是private成员,只能在类内部访问
//cout << A::_scount << endl;
return 0;
// main函数结束时,a1,a2,a3依次析构,_scount变为0
}
/*
* 关键点总结:
* 1. 静态成员变量_scount:
* - 在类内声明,类外初始化
* - 所有对象共享同一个_scount
* - 用于记录程序中A类对象的实时数量
*
* 2. 静态成员函数GetACount():
* - 可以直接通过类名调用(推荐方式)
* - 也可以通过对象调用(语法允许但不推荐)
* - 只能访问静态成员变量(无法访问普通成员变量)
*
* 3. 访问控制:
* - _scount是private成员,外部无法直接访问
* - 必须通过public的GetACount()方法获取
*/