构造函数
构造函数关于初始化列表方面
初始化列表的语法:
初始化列表的语法位于构造函数的参数列表和函数体之间,使用 :
来分隔。
class MyClass
{
public:
int x, y;
// 构造函数使用初始化列表初始化 x 和 y
MyClass(int a, int b) : x(a), y(b) {
// 函数体可以进一步处理
}
};
初始化列表与构造函数体内赋值的区别:
使用初始化列表:
class MyClass {
public:
int x, y;
// 初始化列表初始化成员变量
MyClass(int a, int b) : x(a), y(b) {
// 此时 x 和 y 已经被初始化
}
};
在构造函数体内赋值:
class MyClass {
public:
int x, y;
// 在构造函数体内赋值
MyClass(int a, int b) {
x = a;
y = b;
}
};
虽然看起来这两种方式都能达到相同的效果,但在效率上有所不同。对于内置类型(如 int
、double
),差别不大,但对于复杂类型,初始化列表可以避免不必要的临时对象创建和拷贝操作。
成员变量走初始化列表的逻辑:
显式在初始化列表中初始化的成员变量:
- 如果你在构造函数的初始化列表中明确地为成员变量指定了初始值,那么这些成员变量将按照提供的值进行初始化。
class MyClass {
public:
int x;
MyClass(int a) : x(a) { // x 显式在初始化列表中被初始化为 a
}
};
未显式在初始化列表中的成员变量:
-
如果某些成员变量没有显式在初始化列表中进行初始化,它们的初始化依赖于其声明类型:
类中声明位置有缺省值:
-
如果类内给成员变量提供了默认初始值,那么这些变量将使用该缺省值来初始化
class MyClass {
public:
int x = 10; // 类中声明的缺省值
MyClass() { // 构造函数中没有初始化列表,x 将会使用 10 作为初始值
}
};
类中声明位置没有缺省值:
- 对于内置类型(如
int
、float
等),这些变量的初始值可能是随机的,也可能是 0 或其他默认值,这取决于编译器的行为。 - 对于自定义类型,如果这个类型没有默认构造函数,则编译时会报错;如果有默认构造函数,编译器会调用默认构造函数进行初始化。
class MyClass {
public:
int x; // 没有缺省值,值可能是随机的
MyClass() {
// 没有初始化列表,x 的值是未定义的
}
};
引用成员变量 / const
成员变量 / 没有默认构造函数的成员变量:
- 对于这些类型的成员变量,它们必须通过初始化列表进行初始化,否则编译器会报错。
- 引用类型:引用一旦绑定,不能再更改,所以在对象创建时就必须初始化。
const
类型:常量必须在声明时初始化,因为它们的值在整个对象的生命周期内是不可改变的。- 没有默认构造函数的成员变量:这些类型的成员变量无法使用默认构造函数初始化,所以必须显式初始化。
class MyClass {
public:
const int x; // const 成员
int& y; // 引用成员
MyClass(int a, int& b) : x(a), y(b) { // 必须使用初始化列表
}
};
总结:
- 显式初始化列表:在初始化列表中明确初始化的成员将按指定的值进行初始化。
- 未显式初始化:根据成员的类型(内置类型可能是随机值或 0,自定义类型调用默认构造函数)进行初始化。
- 必须初始化列表的情况:
const
、引用类型成员变量,以及没有默认构造函数的成员变量必须使用初始化列表初始化。
类型转换
在C++中,类型转换(Type Casting)是一种将一个类型的数据转换为另一个类型的机制。这在许多情况下很有用,例如在需要不同精度的算术运算、函数参数传递、类之间的转换等情境下。
1. 隐式类型转换
int a = 10;
double b = a; // 隐式将 int 类型的 a 转换为 double
在上述例子中,编译器会自动将 int
类型的 a
转换为 double
,以确保类型一致。
2. 显式类型转换
C风格类型转换:
C风格的类型转换是最简单的一种方式,但它不推荐在现代 C++ 中使用,因为它不够安全和灵活,无法区分具体的转换种类。
int a = 10;
double b = (double)a; // C 风格的显式类型转换
C++风格类型转换:
C++引入了四种更安全的类型转换运算符,用于替代C风格的转换。
1)
static_cast
2)
dynamic_cast
3)
const_cast
4)
reinterpret_cast
隐式转换和显式转换的对比:
- 隐式转换:自动发生,不需要程序员的干预,但可能导致数据精度丢失或行为不明确。
- 显式转换:程序员手动进行转换,通常用于当隐式转换不能满足需求时。
static成员
• ⽤static修饰的成员变量,称之为静态成员变量,静态成员变量⼀定要在类外进行初始化。
• 静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。
• ⽤static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针。
• 静态成员函数中可以访问其他的静态成员,但是不能访问非静态的,因为没有this指针。
• ⾮静态的成员函数,可以访问任意的静态成员变量和静态成员函数。
• 突破类域就可以访问静态成员,可以通过类名::静态成员或者对象.静态成员来访问静态成员变量 和静态成员函数。
• 静态成员也是类的成员,受public、protected、private访问限定符的限制。
• 静态成员变量不能在声明位置给缺省值初始化,因为缺省值是个构造函数初始化列表的,静态成员 变量不属于某个对象,不⾛构造函数初始化列表
1. static
成员变量
- 共享数据:
static
成员变量是属于整个类的,而不是某个具体对象的。它在所有该类的对象之间共享,且只在内存中存在一份。 - 生命周期:
static
成员变量在程序开始时就分配内存并初始化,直到程序结束时才销毁。 - 作用域:即使在类外也可以通过类名来访问
static
成员变量,而不需要实例化对象。
#include <iostream>
using namespace std;
class MyClass {
public:
static int count; // 声明静态成员变量
MyClass() {
count++;
}
};
int MyClass::count = 0; // 定义并初始化静态成员变量
int main() {
MyClass obj1;
MyClass obj2;
cout << "Number of objects: " << MyClass::count << endl; // 输出2
return 0;
}
在这个例子中,count
是一个静态成员变量,它在类的所有对象之间共享。创建对象obj1
和obj2
时,count
分别递增。
2. static
成员函数
- 不依赖对象:
static
成员函数可以在没有对象的情况下通过类名直接调用。它不能访问非静态成员变量或成员函数,因为它不属于类的任何实例。 - 访问静态成员:
static
成员函数只能访问static
成员变量或其他static
成员函数。
#include <iostream>
using namespace std;
class MyClass {
public:
static int count; // 静态成员变量
static void showCount() { // 静态成员函数
cout << "Count: " << count << endl;
}
};
int MyClass::count = 10;
int main() {
MyClass::showCount(); // 通过类名直接调用静态函数
return 0;
}
在此例子中,showCount
是一个静态成员函数,它访问类的静态成员变量count
,无需对象实例化即可调用。
特性总结:
static
成员变量在所有对象之间共享,且不依赖于任何对象实例。static
成员函数可以通过类名直接调用,且不能访问非静态的成员变量或成员函数。
static
成员适合存储和操作类级别的共享信息,比如计数器、全局配置等。
设已经有A,B,C,D4个类的定义,程序中A,B,C,D构造函数调⽤顺序为?()
设已经有A,B,C,D4个类的定义,程序中A,B,C,D析构函数调⽤顺序为?()
程序结构解析:
-
对象定义:
C c;
是一个全局对象。A a;
是在main()
函数中定义的局部对象。B b;
同样是main()
中的局部对象。static D d;
是main()
函数中的静态对象。
-
构造顺序: C++对象的构造顺序遵循以下规则:
- 全局对象(如
C c
)会在main()
函数执行之前构造。 - 局部对象(如
A a
和B b
)按照它们在代码中出现的顺序依次构造。 - 静态局部对象(如
static D d
)在它们第一次遇到时进行构造,但它们的生命周期是整个程序的运行期间。
- 全局对象(如
-
析构顺序:
- 局部对象的析构顺序与它们的构造顺序相反,即后构造的对象会先析构(遵循“后进先出”的原则)。
- 静态局部对象会在程序结束时(即在
main()
函数执行完毕之后)析构。 - 全局对象会在
main()
函数执行结束后,所有静态局部对象析构完毕后析构。
构造函数调用顺序:
- 全局对象
C
:首先构造,因为它是全局变量,在main()
函数执行之前构造。 - 局部对象
A
:在main()
中第一个被声明,因此紧随其后构造。 - 局部对象
B
:在main()
中第二个被声明,接着构造。 - 静态局部对象
D
:在main()
函数中遇到static D d;
时构造。
析构函数调用顺序:
- 局部对象
B
:B
是局部对象,main()
函数结束时,B
最后构造,最先析构。 - 局部对象
A
:A
是局部对象,在B
之后析构。 - 静态局部对象
D
:D
是静态局部对象,main()
函数结束后析构。 - 全局对象
C
:C
是全局对象,最后析构。
- 构造函数调用顺序:C -> A -> B -> D
- 析构函数调用顺序:B -> A -> D -> C
友元:
• 友元提供了⼀种突破类访问限定符封装的⽅式,友元分为:友元函数和友元类,在函数声明或者类声明的前⾯加friend,并且把友元声明放到⼀个类的⾥⾯。
• 外部友元函数可访问类的私有和保护成员,友元函数仅仅是⼀种声明,他不是类的成员函数。
• 友元函数可以在类定义的任何地方声明,不受类访问限定符限制。
• ⼀个函数可以是多个类的友元函数。
• 友元类中的成员函数都可以是另⼀个类的友元函数,都可以访问另⼀个类中的私有和保护成员。
• 友元类的关系是单向的,不具有交换性,⽐如A类是B类的友元,但是B类不是A类的友元。
• 友元类关系不能传递,如果A是B的友元,B是C的友元,但是A不是C的友元。
• 有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多⽤。
友元的类型:
- 友元函数(Friend Function)
- 友元类(Friend Class)
- 友元成员函数(Member Function as Friend)
1. 友元函数(Friend Function)
友元函数是一个不是类成员的函数,但它可以访问该类的所有私有成员和保护成员。要定义友元函数,可以在类定义中使用 friend
关键字声明。
#include <iostream>
using namespace std;
class MyClass {
private:
int x;
public:
MyClass(int val) : x(val) {}
// 声明友元函数
friend void showValue(MyClass& obj);
};
// 定义友元函数
void showValue(MyClass& obj) {
cout << "x = " << obj.x << endl; // 访问私有成员 x
}
int main() {
MyClass obj(10);
showValue(obj); // 调用友元函数
return 0;
}
showValue()
是MyClass
类的友元函数,它能访问MyClass
的私有成员x
。- 尽管
x
是私有成员,但友元函数可以访问它。
2. 友元类(Friend Class)
友元类允许一个类访问另一个类的所有私有和保护成员。在类定义中,可以使用 friend
关键字声明另一个类为其友元类。
#include <iostream>
using namespace std;
class MyClass {
private:
int x;
public:
MyClass(int val) : x(val) {}
// 声明 FriendClass 是 MyClass 的友元类
friend class FriendClass;
};
class FriendClass {
public:
void showValue(MyClass& obj) {
cout << "x = " << obj.x << endl; // 访问私有成员 x
}
};
int main() {
MyClass obj(20);
FriendClass fObj;
fObj.showValue(obj); // 通过友元类访问私有成员
return 0;
}
FriendClass
是MyClass
的友元类,因此它能够访问MyClass
中的私有成员x
。FriendClass::showValue()
访问了MyClass
的私有成员x。
3. 友元成员函数(Member Function as Friend)
你还可以将另一个类的某个成员函数声明为当前类的友元。这允许特定的成员函数访问当前类的私有成员,而不是整个类。
#include <iostream>
using namespace std;
class ClassB; // 前向声明
class ClassA {
private:
int x;
public:
ClassA(int val) : x(val) {}
// 声明 ClassB 的某个成员函数是 ClassA 的友元
friend void ClassB::show(ClassA& obj);
};
class ClassB {
public:
void show(ClassA& obj) {
cout << "x = " << obj.x << endl; // 访问 ClassA 的私有成员 x
}
};
int main() {
ClassA objA(30);
ClassB objB;
objB.show(objA); // 通过 ClassB 的成员函数访问 ClassA 的私有成员
return 0;
}
ClassB
的show()
函数是ClassA
的友元成员函数,因此它可以访问ClassA
的私有成员x
。ClassB
本身不是ClassA
的友元类,只有show()
函数有权访问ClassA
的私有成员。
注意事项:
-
友元破坏封装性:友元虽然方便,但它也破坏了面向对象编程的封装性原则,因此应谨慎使用。滥用友元会导致代码的维护变得困难,因为它打破了类之间的清晰边界。
-
友元不是继承的一部分:友元关系是非对称和非传递的。如果
ClassA
是ClassB
的友元,ClassB
不会自动成为ClassA
的友元。类似地,友元关系不会在继承中传递。 -
使用友元需慎重:尽量保持类的成员变量私有,只有在确实需要其他函数或类直接访问内部数据时才使用友元。这是一种增强代码灵活性的方法,但应在设计时权衡其副作用。
总结:
- 友元函数:允许非成员函数访问类的私有成员。
- 友元类:允许另一个类访问当前类的所有成员。
- 友元成员函数:允许特定的成员函数访问类的私有成员。
- 友元提供了灵活的访问控制方式,但应慎用,以维护良好的封装性和代码结构。
内部类:
• 如果⼀个类定义在另⼀个类的内部,这个内部类就叫做内部类。内部类是⼀个独⽴的类,跟定义在 全局相⽐,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。
• 内部类默认是外部类的友元类。
• 内部类本质也是⼀种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使⽤,那么可以考 虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其 他地⽅都⽤不了
求1+2+3+...+n_牛客题霸_牛客网 这题可以让我们更加了解什么是内部类
class Solution {
// 内部类
class Sum
{
public:
Sum()
{
_ret += _i;
++_i;
}
};
static int _i;
static int _ret;
public:
int Sum_Solution(int n)
{
// 变⻓数组
Sum arr[n];
return _ret;
}
};
int Solution::_i = 1;
int Solution::_ret = 0;
匿名对象
匿名对象(Anonymous Object)是指没有名字的对象,即对象在创建时不分配一个显式的变量名。这类对象通常是临时对象,创建后会立即用于某个操作,且在操作完成后就会销毁。匿名对象一般在函数调用、返回值或赋值时出现,并且生命周期非常短。
匿名对象的使用场景:
-
函数的临时返回值: 当函数返回一个对象时,往往返回的是一个匿名对象。这个匿名对象可以被赋值给另一个变量,也可以作为临时对象直接使用。
-
临时对象的创建: 可以直接通过构造函数创建一个匿名对象,匿名对象只会在表达式的上下文中存活,使用完之后会立即销毁。
匿名对象的特征:
- 没有名称:匿名对象没有名字,因此你无法直接引用它,通常只能在表达式中使用。
- 生命周期短:匿名对象的生命周期非常短,只在当前的表达式范围内存活,表达式结束后,匿名对象就会被销毁。
- 临时性:它们是临时对象,通常用于一些不需要长期保存的场景。
匿名对象的例子:
1. 函数返回匿名对象:
#include <iostream>
using namespace std;
class MyClass {
public:
MyClass() {
cout << "构造函数被调用" << endl;
}
~MyClass() {
cout << "析构函数被调用" << endl;
}
};
MyClass createObject() {
return MyClass(); // 返回一个匿名对象
}
int main() {
MyClass obj = createObject(); // 调用构造和析构函数
return 0;
}
2. 直接创建匿名对象:
#include <iostream>
using namespace std;
class MyClass {
public:
MyClass() {
cout << "构造函数被调用" << endl;
}
~MyClass() {
cout << "析构函数被调用" << endl;
}
};
int main() {
MyClass(); // 创建了一个匿名对象
cout << "匿名对象创建完毕" << endl;
return 0;
}
3. 作为函数参数的匿名对象:
#include <iostream>
using namespace std;
class MyClass {
public:
MyClass() {
cout << "构造函数被调用" << endl;
}
~MyClass() {
cout << "析构函数被调用" << endl;
}
};
void func(MyClass obj) {
cout << "函数体内" << endl;
}
int main() {
func(MyClass()); // 传递一个匿名对象作为参数
return 0;
}
匿名对象的作用:
-
减少内存开销: 匿名对象通常用于那些只需要临时存在的对象场景。它们在创建之后立即使用,使用完后销毁,这样就不会占用多余的内存。
-
简化代码: 匿名对象可以让代码更加简洁,因为不需要为临时对象定义名称,直接使用对象的构造函数创建并使用。
-
优化性能: 现代C++编译器支持的返回值优化(RVO)和移动语义可以减少匿名对象的开销。尤其是通过移动构造函数,将匿名对象的资源“移动”到目标对象,而不是进行拷贝。
匿名对象的生命周期:
- 临时对象的创建:当表达式或函数需要时,匿名对象会立即创建。
- 临时对象的销毁:一旦表达式结束,匿名对象会被立即销毁。析构函数会自动被调用。
匿名对象的注意事项:
-
生命周期短:由于匿名对象的生命周期非常短,可能在你希望使用它之前就已经销毁了。因此,匿名对象不适合存储或传递给需要长期使用的对象。
-
RVO(返回值优化):C++ 编译器可以通过优化,避免拷贝和构造不必要的临时对象。例如,在函数返回匿名对象时,编译器可能直接将返回值“构造”在目标位置,而不创建中间对象。
-
无法直接引用:匿名对象没有名称,无法在程序的其他地方直接引用它,只能通过当前的上下文使用它。
总结:
- 匿名对象 是一种不具名的临时对象,通常在函数返回值、参数传递和临时计算时使用。
- 生命周期非常短,它们在表达式结束后立即销毁,析构函数会自动调用。
- 减少了冗余对象的创建,有助于简化代码并优化性能。
对象拷⻉时的编译器优化
在C++中,当对象拷贝时,编译器可以进行一些优化来减少不必要的对象构造和销毁操作,尤其是对于临时对象和返回值的拷贝。主要的编译器优化机制包括:
- 返回值优化(RVO)和命名返回值优化(NRVO)
- 移动语义和移动构造函数
- 拷贝省略(Copy Elision)
1. 返回值优化(RVO)和命名返回值优化(NRVO)
返回值优化(RVO) 是编译器为避免不必要的临时对象拷贝而进行的一种优化技术。当一个函数返回对象时,编译器可以直接在调用代码的目标位置构造返回的对象,而不是创建临时对象再拷贝。这个优化在C++98中就已经存在,并且在C++17中被强制要求成为标准行为,不再需要编译器特定的优化。
命名返回值优化(NRVO) 是RVO的变体,用于处理函数中定义的具名变量的返回优化。编译器可以直接在返回值位置构造这个具名变量,从而避免额外的拷贝和构造操作。
2. 移动语义和移动构造函数
C++11引入了移动语义,包括移动构造函数和移动赋值运算符。移动语义允许编译器将资源从一个临时对象“移动”到目标对象,而不是进行昂贵的拷贝操作。这对于避免不必要的深拷贝非常有用,尤其是在处理动态内存、文件句柄等资源时。
3. 拷贝省略(Copy Elision)
拷贝省略 是C++中最常见的优化之一。即使没有移动语义,编译器也可以直接跳过拷贝操作(拷贝构造函数的调用),并直接将对象构造在目标内存位置。这与RVO和NRVO类似,但它不仅适用于返回值场景,也适用于对象赋值和传递的其他场景。