一、初始化列表
初始化列表是一种在构造函数中使用的特殊语法,用于在创建对象时初始化类的成员变量。它由冒号后跟一系列以逗号分隔的初始化字段组成。
1.语法
构造函数 ( ) :属性1 (值1) ,属性2 (值2) …{ }
2.注意事项
①const成员和引用成员:
const数据成员和引用数据成员必须在初始化列表中初始化,因为它们一旦构造就不能被改变。
②其他类的对象成员:
当类中包含其他类的对象作为成员变量时,可以使用初始化列表来调用这些对象类的构造函数,以确保它们被正确初始化。
③调用基类构造函数:
如果类有继承结构,并且基类没有默认构造函数,则必须在派生类的初始化列表中显式调用基类的构造函数。
④默认构造函数的使用:
如果成员变量有默认构造函数,可以在初始化列表中通过空括号()来调用它。对于内置类型,如果不提供初始值,则会被默认初始化。
⑤成员变量的初始化顺序:
成员变量的初始化顺序是按照它们在类中声明的顺序进行的,而不是初始化列表中出现的顺序。
⑥初始化效率:
使用初始化列表初始化成员变量通常比在构造函数体内部赋值更高效,特别是在类作为成员变量时,因为它避免了对象的临时复制和潜在的多次构造。
class Test {
public:
const int T_a;
int T_b;
int& T_c;
//传统赋值操作
/*
Test(int a, int b, int c) {
T_a = a;
T_b = b;
T_c = c;
}
*/
//初始化列表
Test(int a, int b, int& c) :T_a(a), T_b(b), T_c(c) {
}
};
int x = 30;
Test t1(10, 20, x);
cout << t1.T_a << " ";
cout << t1.T_b << " ";
cout << t1.T_c << endl;
//输出 10 20 30
二、类的成员
1.类内声明类外实现
类内声明成员函数时,通常只提供函数的原型,即函数的返回类型、函数名和参数列表。
类外实现成员函数时,需要在返回值后面使用作用域解析运算符::来指定函数所属的类,并提供实现函数的代码。
class MyClass
{
public:
int MyFunction(int value);
};
int MyClass::MyFunction(int value)
{
return value;
}
①类内声明与类外实现的好处:
- 将声明和定义分开使类的接口更清晰,提高代码组织性。
- 隐藏实现细节有助于保护类的内部状态,减少对外部的依赖。
- 如果需要修改成员函数的实现,只需更改类外的实现文件,而不必修改包含类声明的头文件。
- 在大型项目中,通常将类的声明放在头文件中,实现放在源文件中,这有助于管理编译单元和减少编译时间。
②注意事项:
- 确保类内声明的成员函数在类外有唯一的实现,避免链接错误。
- 如果成员函数在类内被声明为inline,则在类外实现时也应使用inline关键字,以避免出现多重定义问题。
2.类对象作为类成员
C++中一个类的成员可以是另一个类的对象,我们称该成员为对象成员。
①构造和析构的顺序:
当一个类中包含其他类的对象作为成员时,构造函数的调用顺序是先构造成员对象,再构造本类对象。析构函数的调用顺序与构造相反,即先析构本类对象,再析构成员对象。
②初始化和赋值:
在构造函数中,可以通过初始化列表调用成员对象的构造函数,完成对成员对象的初始化。如果成员对象在类中没有被显式初始化,那么它们将使用默认构造函数进行初始化。
class Phone {
public:
Phone(const string& name) :Phone_name(name) {
cout << "Phone" << endl;
}
Phone() {
Phone_name = "NULL";
}
~Phone(){
cout << "~Phone" << endl;
}
private:
string Phone_name;
};
class Person {
public:
Person(const string& name, const string& p)
:Person_name(name),phone(p) {
cout << "Person" << endl;
}
Person(){
Person_name = "NULL";
}
~Person() {
cout << "~Person" << endl;
}
private:
string Person_name;
Phone phone;
};
Person p1("Tom", "phone");
//输出结果:
Phone
Person
~Person
~Phone
3.引用成员
引用数据成员特点:
- 引用数据成员不能被重新绑定到另一个对象上。
- 引用数据成员必须在构造函数的初始化列表中初始化。
- 引用数据成员的类不能拥有默认构造函数,因为默认构造函数无法初始化引用。
- 引用数据成员的构造函数的参数必须是引用类型,确保传给引用的是一个已经存在的对象。
4.静态成员
静态成员变量和静态函数是C++中用于实现数据共享和特定操作的类成员。
①静态成员变量:
使用static关键字修饰的类成员变量称为静态成员变量。它们属于类,而不是类的某个具体对象。
特点:
- 静态成员变量是类的所有对象共享的,即所有对象都访问和修改同一份数据。
- 静态成员变量需要在类内声明,类外初始化,因为它在编译阶段分配内存。
- 如果没有在初始化时赋值,静态成员变量会被默认初始化为0。
- 静态成员变量可以通过类名直接访问,也可以通过对象名访问。
②静态成员函数:
使用static关键字修饰的成员函数称为静态成员函数。它们与类相关联,而不是与类的某个具体对象相关联。
特点:
- 静态成员函数只能访问静态成员变量和其他静态成员函数。
- 静态成员函数可以通过类名和作用域解析运算符来调用,也可以通过类的对象来调用。
- 静态成员函数不依赖于类的实例,因此它们没有this指针。
- 静态成员函数的使用主要是为了管理静态数据成员,完成对静态数据成员的封装。
- 静态成员函数常用于实现某些设计模式,如单例模式,其中静态成员函数用于控制对象的创建和访问。
class Counter {
public:
static int cnt;
int t;
Counter() {
cnt++;
}
static void printcnt() {
cout << cnt << endl;
//cout << t << endl; //错误
}
};
//int Counter::cnt; //默认为0
int Counter::cnt = 0;
Counter c1, c2;
c1.printcnt(); //对象名访问
Counter* c3 = new Counter();
Counter::printcnt(); //类名访问
delete c3;
//输出
2
3
在类外实现静态成员时不需要加static关键字。
三、对象数组、对象模型和this指针
1.对象数组
对象数组是同类型对象的集合,每个数组元素本身就是一个对象。
对象数组的概念类似于结构体数组,但是对象数组中的元素拥有类的特性,包括构造函数、析构函数及成员变量和成员函数。
class Point {
public:
int P_x, P_y;
Point(int x, int y) :P_x(x), P_y(y) {
}
Point() :P_x(0), P_y(0) {
}
void print() const {
cout << "(" << P_x << "," << P_y << ")" << endl;
}
};
Point p[3] = { Point(1,2),Point(3,3) };
for (int i = 0; i < 3; i++) {
p[i].print();
}
//输出
(1,2)
(3,3)
(0,0)
注意事项:
- 对象数组中的每个元素都会调用构造函数进行初始化。
- 可以在声明数组时提供初始化列表来初始化数组中的某些或全部元素。
- 如果数组声明时未提供初始化列表,未显式初始化的对象将调用默认构造函数进行初始化。
- 如果在堆上动态分配对象数组,则要适时使用delete[ ]操作符来释放内存。
2.对象模型
对象模型主要由以下几个部分组成:
①成员变量和成员函数:
成员变量和成员函数是分开存储的。静态成员不属于类的对象,只有非静态成员变量才属于类的对象,也就是说只有非静态成员变量的大小算进类的大小中。
如果类中存在虚函数,该类还会分配一个指针的空间去存放指向虚表的指针。虚表中包含了类的所有虚函数的地址信息。
②对象的内存布局:
空对象占用1个字节的空间,这是为了区分空类实例化的对象在内存中的位置。每个空对象都有一个独一无二的内存地址。
对于包含非静态成员变量的对象,其内存大小取决于成员变量的数量和类型。
③成员函数的调用:
this指针可以解决成员函数如何区分不同对象的问题。this指针指向被调用的成员函数所属的对象。
3.this指针
每一个非静态成员函数只产生一份函数实例,即多个同类型的对象会使用同一块代码。
问题是:这块代码是如何区分到底是哪个对象调用自己的呢?C++提供this指针来解决这个问题。
①this指针的概念:
this指针是一个隐含于非静态成员函数中的特殊指针,它指向被调用的成员函数所属的对象。
当一个对象调用其成员函数时,编译器会自动将对象的地址通过this指针传递给函数,使得成员函数能够访问对象的成员变量。
②this指针的作用:
- 区分不同对象:当多个对象调用同一个成员函数时,this指针能够帮助函数区分是哪个对象调用的,从而对正确的对象进行操作。
- 解决名称冲突:当成员函数中的参数名与成员变量名相同时,可以使用this指针来区分。
- 返回对象本身:在成员函数中,可以使用 return *this; 来返回对象本身,这种方法常用于实现链式调用。
在成员函数内部,可以通过this指针来访问对象的成员变量,尽管通常情况下可以直接使用成员变量名,因为编译器会自动添加this指针。
4. this指针的特性:
- this指针是自动生成的,不需要定义,每个非静态成员函数都有一个this指针。
- 一般来说,所有的成员函数都将this指针设置为调用它的对象的地址。
- this指针是一个指针常量,即不能修改this指针所指向的对象,但可以修改其指向的对象的数据成员。
class Button {
private:
string label;
bool isPressed;
public:
Button(const string& label)
:label(label), isPressed(false) {}
Button()
:label("default"), isPressed(false) {}
void setLabel(const string& label) {
this->label = label;
}
void onClick() {
this->isPressed = true;
}
bool wasPressed() const {
return isPressed;
}
};
Button b1;
Button b2("Cancel");
b1.setLabel("Accept");
b1.onClick();
if (b1.wasPressed()) {
cout << "b1 was pressed." << endl;
}
if (b2.wasPressed()) {
cout << "b2 was pressed." << endl;
}
//输出
b1 was pressed.
四、成员函数和空指针与const
1.空指针调用成员函数
C++中的空指针可以调用成员函数,但需要注意以下几点:
①成员函数的调用:
空指针可以调用一般的成员函数(包括静态成员函数),但不能调用虚成员函数,因为虚函数调用涉及到虚函数表,而空指针无法提供有效的虚函数表地址。
②this指针的使用:
如果成员函数使用了this指针,空指针调用该函数时可能会导致程序崩溃。因此需要对this指针进行判断,以确保代码的健壮性。
class Test {
public:
void Func() {
if (this == nullptr) {
cout << "this == nullptr" << endl;
return;
}
cout << "void Func()调用" << endl;
}
};
Test* t1 = nullptr;
t1->Func();
//输出this == nullptr
2.const修饰成员函数
C++中的const关键字可以用来修饰成员函数,以表明该函数不会修改类的任何成员变量。
①常函数的声明和使用:
在成员函数的声明末尾加上const关键字,表示该函数为常函数。
特点:
- 常函数内部不能修改任何非mutable成员变量,但可以读取和访问成员变量。
- 常函数可以调用其它const成员函数,但不能调用普通成员函数,因为普通成员函数可能会修改成员变量。
- const成员函数可以与其非const版本共存,因为它们具有不同的常量性。编译器会根据对象是否为const来决定调用哪个版本的函数。
- 只要类方法不修改调用对象,就应将其声明为const。
- 在多线程编程中,const成员函数可以帮助识别哪些操作是线程安全的,因为它们不会修改共享的数据。
②常对象调用常函数:
声明对象前加上const关键字,这个对象就是常对象。
常对象只能调用常函数,不能调用普通成员函数,因为普通成员函数可能修改对象的属性。
class Test {
public:
static const int a;
void Func() const {
cout << a << endl;
}
void Func() {
cout << "void Func()" << endl;
}
};
const int Test::a = 5;
Test t1;
t1.Func(); //输出void Func()
const Test t2;
t2.Func(); //输出5
3.mutable关键字
C++中的mutable关键字用于修饰类的成员变量,使其可以在常函数中被修改。
特点:
- mutable不能用于修饰静态成员变量,也不能与const同时修饰成员变量。
- mutable可用于在lambda表达式中修改变量,这在通常情况下是不允许的,因为lambda表达式捕获的变量是按值传递的。
- 在设计类时,应尽量避免使用mutable关键字,因为它可能会破坏类的封装性。
class Test {
public:
mutable int a = 0;
void Func() const {
a = 5;
cout << a << endl;
}
};
Test t1;
t1.Func();
//输出5
4.const和宏定义区别
①编译器处理方式:
- const常量:编译阶段,占用内存空间,存储在数据段中。
- 宏定义:预处理阶段,不占用内存空间,只是在代码中展开,可能导致代码膨胀。
②类型和安全检查:
- const常量有具体的类型,编译器会执行类型检查,确保类型安全。
- 宏定义没有类型,只是简单的文本替换,不进行类型检查。
③定义域和生命周期:
- const常量受定义域限制,仅在定义的代码块中有效。
- 宏定义不受定义域限制,可以在整个程序中使用。
④取消定义:
- const常量一旦定义,不能被取消定义或重新定义。
- 宏定义可以通过#undef取消定义,也可以重新定义。
五、友元
C++中的友元机制允许一个类授予指定的函数或类对其非公有成员的访问权。
友元声明必须在类的定义内部进行,但并不限制其在类的哪个部分声明。
1.友元函数
当一个普通函数被声明为某个类的友元时,它可以访问该类的私有和保护成员,就好像它是该类的一部分一样。
特点:
- 非成员函数:友元函数是普通的全局函数,而不是类的成员函数。
- 声明格式:friend关键字加目标函数的完整声明。
注意:
- 友元函数可以绕过类的公共接口直接访问其非公有数据,这破坏了类的封装性。
- 过度使用友元函数可能会使代码维护变得复杂,因为它增加了类的外部依赖。
class Rectangle {
friend void print(Rectangle& rect);
private:
int length;
protected:
int width;
public:
Rectangle(int a, int b) :length(a), width(b) {
}
Rectangle() :length(0), width(0) {
}
};
void print(Rectangle& rect) {
cout << rect.length << " " << rect.width << endl;
}
Rectangle r1(2,3);
print(r1);
//输出2 3
2.友元类
当一个类被声明为某个类的友元时,它的所有成员函数都可以访问声明它为友元的那个类的私有和保护成员。
特点:
- 声明格式:friend关键字加目标类的完整声明。
- 性质:友元类的关系是单向的,没有交换性和传递性,且不会被继承。
友元类通常用于以下场景:
- 类的专用工具类:当一个类需要一个专门的工具类来处理其内部数据时,可以将该工具类声明为友元类。
- 类的工厂模式:在工厂模式中,友元类可以用来创建和初始化目标类的对象。
class Box {
friend class Friend;
private:
int data;
public:
Box(int value) :data(value) {
}
Box() :data(0) {
}
};
class Friend {
public:
void showdata(const Box& box) {
cout << box.data << endl;
}
};
Box box(10);
Friend fed;
fed.showdata(box);
//输出10
3.友元成员函数
当一个类的成员函数被声明为另一个类的友元时,该成员函数可以访问另一个类的私有和保护成员。
- 声明格式:friend关键字加目标成员函数的完整声明,并在返回值后面加上类的作用域。
- 性质:当成员函数的参数为声明它为友元的那个类的对象时,可以先在类内声明,在类外等对应的类创建完成后再实现。
class Home;
class GoodFriend {
public:
void visit(const Home& home);
};
class Home {
friend void GoodFriend::visit(const Home& home);
private:
string bedroom;
public:
Home(const string& value) :bedroom(value) {
}
Home() :bedroom("default") {
}
};
void GoodFriend::visit(const Home& home)
{
cout << home.bedroom << endl;
}
Home my_home("my_room");
GoodFriend friend1;
friend1.visit(my_home);
//输出my_room