csdn推荐
隐藏友元举例:
class MyClass {
private:
int privateData;
public:
friend void externalFriendFunction() {
MyClass mc;
mc.privateData = 40; // 直接访问私有成员
}
};
int main()
{
externalFriendFunction();
//此时,报错 error: 'externalFriendFunction' was not declared in this scope
}
既然常规的名称查找查找不到,则使用ADL(参数依赖查找)作为隐藏友元的正确使用方式。
// 类内定义友元函数
class MyClass {
private:
int privateData;
public:
friend void externalFriendFunction(MyClass& mc){
mc.privateData = 40; // 直接访问私有成员
}
};
int main()
{
MyClass myc;
externalFriendFunction(myc);
}
四、构造、析构与复制成员函数 1.构造函数
在C++中,构造函数是一种特殊的成员函数,用于在创建对象时初始化对象的状态。以下是构造函数的一些关键特性:
代理构造函数(C++11):
C++11标准引入了一种新的构造函数特性,称为委托构造函数或代理构造函数。这种特性允许一个构造函数调用同一个类中的另一个构造函数来完成其初始化工作。这在有多个构造函数且一些构造函数有共同初始化代码的情况下非常有用。
代理构造函数的优点:
其语法如下:
class MyClass {
public:
MyClass(int x) : value(x) {} // 构造函数1
MyClass(const std::string& s) : MyClass(s.size()) {} // 代理构造函数
private:
int value;
};
在这个例子中,MyClass有两个构造函数。第二个构造函数接受一个std::string类型的参数,并使用成员初始化列表委托给第一个构造函数,传递s.size()作为参数。
初始化列表:
在C++中,初始化列表(Initializer List)是一种特殊的语法,用于在构造函数中初始化对象的非静态数据成员。
初始化列表用于初始化,而不是赋值。
区分数据成员的初始化与赋值:
一些情况下必须使用初始化列表
初始化顺序
成员的初始化顺序是根据它们在类中声明的顺序,而不是初始化列表中的顺序。
使用初始化列表覆盖类内成员初始化的行为
在类的定义中,可以为成员变量提供默认的初始化值。然而,当构造函数中提供了初始化列表时,这些默认初始化值会被覆盖。
class MyClass {
public:
MyClass(int x) : value(x) {} // 使用初始化列表初始化value
private:
int value = 10; // 类内成员初始化,但会被初始化列表覆盖
};
缺省构造函数:(零个参数)
在C++中,缺省构造函数是指不需要提供任何参数即可调用的构造函数 ,它允许类的实例在没有提供具体初始化参数的情况下被创建。缺省构造函数的特性:
单一参数构造函数:
class MyClass {
public:
explicit MyClass(int value) : x(value) {
// 构造函数体
}
private:
int x;
};
int main() {
MyClass obj(10); // 正确,显式调用
MyClass obj1 = MyClass(10); // 正确,显式类型转换后再将临时对象复制到obj
MyClass obj2 = static_cast<MyClass>(10); //单一构造函数中的参数类型与10一致就可以使用static_cast完成类型转换
// MyClass obj3 = 10; // 错误,因为构造函数是explicit的,不能用于隐式转换
}
在C++中,单一参数的构造函数确实可以被看作是一种类型转换函数。这是因为它们允许将一个类型的值转换为另一个类型的对象。
然而,这种转换可能会在不经意间发生,导致一些不期望的行为。为了避免这种情况,C++提供了explicit关键字,可以使用explicit关键字避免求值过程中的隐式转换。当你在一个构造函数前使用explicit关键字时,你告诉编译器这个构造函数不能用于隐式类型转换。这意味着,它不能在没有明确调用的情况下自动将参数转换为类的对象。
拷贝构造函数:接收一个当前类对象的构造函数
C++中的拷贝构造函数是一个特殊的构造函数,它负责创建一个对象的副本。拷贝构造函数通常在以下几种场景中被调用:
参数传递:当一个对象作为参数传递给函数时,如果函数参数的类型与对象类型相同,拷贝构造函数会被调用以创建一个副本。返回值:当一个函数返回一个对象时,拷贝构造函数会被用来创建返回对象的副本。对象复制:当使用=操作符显式复制对象时,拷贝构造函数会被调用。数组初始化:当使用数组初始化语法创建对象数组时,拷贝构造函数会被用来初始化数组中的每个元素。
拷贝构造函数会在涉及到拷贝初始化的场景被调用,因此要注意拷贝构造函数的形参类型。拷贝构造函数的形参类型通常是对象类型的引用,并且通常需要加上const修饰符,以防止对原始对象的修改。例如:
class MyClass {
private:
int* data;
size_t size;
public:
MyClass(const MyClass& other) : size(other.size), data(new int[other.size]) {
// 拷贝构造函数体,复制other的数据成员
}
};
如果开发者没有为类提供拷贝构造函数,编译器会自动合成一个默认的拷贝构造函数。合成的拷贝构造函数会逐个成员地调用成员的拷贝构造函数来复制对象。这意味着,如果类中包含有自定义的拷贝逻辑,开发者需要显式地提供拷贝构造函数,以确保正确地复制对象。
合成拷贝构造函数的行为适用于所有成员,包括基本数据类型、指针、引用和其他对象。如果类中包含指针,开发者通常需要特别注意拷贝构造函数的行为,因为默认的拷贝构造函数会执行浅拷贝,这可能导致两个对象共享相同的资源,从而引发问题。在这种情况下,需要自定义拷贝构造函数来实现深拷贝。
示例,如果MyClass包含一个指向动态分配内存的指针,拷贝构造函数可能需要这样实现
#include
#include // 用于std::memcpy
class MyClass {
private:
int* data;
size_t size;
public:
// 普通构造函数
MyClass(size_t sz) : size(sz), data(new int[sz]) {
std::cout << "构造函数被调用,分配内存。" << std::endl;
for (size_t i = 0; i < size; ++i) {
data[i] = i; // 初始化数组
}
}
// 拷贝构造函数
MyClass(const MyClass& other) : size(other.size), data(new int[other.size]) {
std::cout << "拷贝构造函数被调用,进行深拷贝。" << std::endl;
std::memcpy(data, other.data, size * sizeof(int));
}
// 析构函数
~MyClass() {
std::cout << "析构函数被调用,释放内存。" << std::endl;
delete[] data;
}
// 辅助函数,用于打印数组内容
void print() const {
std::cout << "数组内容:";
for (size_t i = 0; i < size; ++i) {
std::cout << data[i] << " ";
}
std::cout << std::endl;
}
};
int main() {
MyClass a(5); // 创建对象a
a.print(); // 打印对象a的内容
MyClass b = a; // 使用拷贝构造函数创建对象b,进行深拷贝
b.print(); // 打印对象b的内容
return 0;
}
运行结果:
构造函数被调用,分配内存。
数组内容:0 1 2 3 4
拷贝构造函数被调用,进行深拷贝。
数组内容:0 1 2 3 4
析构函数被调用,释放内存。
析构函数被调用,释放内存。
拷贝构造函数首先为新对象分配内存,然后使用std::memcpy函数来复制原始对象的数组内容。这样,每个MyClass对象都有自己独立的内存副本,避免了浅拷贝可能带来的问题。
移动构造函数(C++11):接收一个当前类右值引用对象的构造函数
移动构造函数是C++11引入的一个特性,它允许开发者优化资源的转移,特别是对于大型对象或者拥有资源(如动态分配的内存)的对象。移动构造函数接收一个右值引用作为参数,这个右值引用通常指向一个临时对象或者即将被销毁的对象。
有移动构造函数就调用移动构造函数,没有移动构造函数就调用拷贝构造函数。
以下是移动构造函数的一些关键点:
资源转移:移动构造函数可以从传入的对象中“偷窃”资源,例如指针指向的内存。这避免了复制资源的开销。
合法状态:在转移资源后,需要确保原对象处于合法状态。这通常意味着原对象不应该再持有被转移的资源。
编译器合成:如果类没有定义时(如拷贝构造函数),编译器可以合成一个。
在什么情况下才会合成,后面会详细介绍
异常保证:移动构造函数通常声明为noexcept,即保证不会抛出异常。这是因为移动操作通常涉及资源的转移,如果抛出异常,可能会导致资源泄漏。
右值引用:移动构造函数接收右值引用,右值引用是C++11引入的,用于标识那些即将离开作用域的对象。
左值和右值:在C++中,右值引用对象用作表达式时实际上是左值,因为它们有确定的内存地址。它们表示临时对象或即将销毁的对象。
示例:
#include
class MyClass {
public:
int* data;
MyClass(int size) {
data = new int[size];
// 初始化data等操作
}
// 移动构造函数
MyClass(MyClass&& other) noexcept : data(other.data) {
// 将资源从other“偷”过来
other.data = nullptr; // 确保other处于合法状态
}
~MyClass() {
delete[] data;
}
};
int main() {
MyClass a(10);
MyClass b = std::move(a); // 使用移动构造函数创建b,a的数据被转移到b
// a.data现在是nullptr,b.data指向原来的数据
}
拷贝赋值与移动赋值函数(operator=):运算符重载
在C++中,拷贝赋值运算符(operator=)和移动赋值运算符是两个重要的成员函数,它们允许对象之间赋值。
拷贝赋值运算符:
功能:用于将一个对象的内容复制到另一个对象中。自赋值检查:需要检查是否发生自赋值,即对象赋值给自己。如果发生自赋值,需要先复制一份再进行赋值,以避免覆盖原始数据。返回类型:通常返回当前对象类型的引用,允许链式赋值。合成:如果类没有定义拷贝赋值运算符,编译器会自动合成一个。
移动赋值运算符:
功能:类似于移动构造函数,用于从临时对象或即将销毁的对象中“偷”资源。自赋值检查:同样需要检查自赋值。返回类型: 返回当前对象类型的引用。合成:如果类没有定义移动赋值运算符,并且满足某些条件,编译器会自动合成一个。
共同点:
示例:见3
2.析构函数
C++中的析构函数是一个特殊的成员函数,其主要作用是释放对象生命周期结束时占用的资源。以下是析构函数的一些关键特性:
函数名:析构函数的名称由类型名称前加上~符号组成,例如,对于类MyClass,其析构函数名为~MyClass()。无参数和无返回值:析构函数不接受任何参数,并且没有返回值。释放资源:析构函数的主要目的是释放对象在构造时或在其生命周期中分配的资源,如动态内存、文件句柄、网络连接等。内存回收:在析构函数执行完毕后,对象所占用的内存才会被回收。这意味着析构函数中执行的所有清理工作必须在内存回收之前完成。自动合成:如果开发者没有为类定义析构函数,编译器会自动合成一个默认的析构函数。合成的析构函数通常执行一些基本的清理工作,例如调用类成员的析构函数。异常保证,不能抛出异常:析构函数通常被声明为noexcept(或在C++11之前使用throw()),这意味着它们保证不会抛出异常。这是重要的,因为在异常处理过程中,析构函数可能会被调用,如果在这种情况下抛出异常,程序将无法正确地清理资源。继承和多态:在继承体系中,基类的析构函数应该是虚函数(特别是当基类指针被用来指向派生类对象时)。这确保了当删除基类指针时,正确的析构函数会被调用。自定义析构函数:如果类管理了动态分配的资源,或者有其他需要在对象生命周期结束时执行的清理工作,开发者需要自定义析构函数来确保资源的正确释放。
示例:
#include
class MyClass {
public:
MyClass() {
// 构造函数逻辑,可能包括动态内存分配
}
~MyClass() {
// 析构函数逻辑,释放资源
std::cout << "资源正在被释放。" << std::endl;
}
};
int main() {
{
MyClass obj; // obj的构造函数被调用
// ...
} // obj的析构函数在对象生命周期结束时被调用,自动释放资源
return 0;
}
3.补充
通常来说,一个类:
示例:包含指针的类
#include
#include // 用于std::memcpy
class MyClass {
public:
MyClass() : data(new int()) {}
// 拷贝构造函数
MyClass(const MyClass& other) : data(new int())
{
std::cout << "copy constructor is called!" << std::endl;
//分配新的内存
*data = *(other.data);
}
// 拷贝赋值运算符
MyClass& operator= (const MyClass& other)
{
std::cout << "copy assignment is called!" << std::endl;
if (this != &other)
{
*data = *(other.data);
}
return *this;
}
// 移动构造函数
MyClass(MyClass&& other) noexcept
: data(other.data)
{
std::cout << "move constructor is called!" << std::endl;
other.data = nullptr;
}
// 移动赋值运算符
MyClass& operator=(MyClass&& other) noexcept {
std::cout << "move assignment is called!" << std::endl;
if (this != &other)
{
data = other.data;
other.data = nullptr;
}
return *this;
}
~MyClass() {
std::cout << "destructor is called!" << std::endl;
delete data;
}
int& getData() {
return *data;
}
private:
int* data;
};
int main()
{
MyClass obj;
obj.getData() = 3;
std::cout << obj.getData() << std::endl;
//拷贝构造,需要开辟资源
MyClass obj1(obj);
//拷贝赋值
obj1 = obj;
//移动构造,不需要开辟资源
MyClass obj2 = std::move(obj1);
//移动赋值函数
MyClass obj3;
obj3 = std::move(obj2);
}
运行结果:
3
copy constructor is called!
copy assignment is called!
move constructor is called!
move assignment is called!
destructor is called!
destructor is called!
destructor is called!
destructor is called!
default关键字:
在C++中,default 关键字用于指示编译器为类自动生成默认的特殊成员函数。这些特殊成员函数包括:
默认构造函数:如果没有为类定义任何构造函数,编译器将自动提供一个默认构造函数。拷贝构造函数:如果没有为类定义拷贝构造函数,并且类没有声明任何成员为 const 或引用类型,编译器将自动提供一个默认拷贝构造函数。移动构造函数:如果没有为类定义移动构造函数,并且类的所有非静态成员都可以移动,编译器将自动提供一个默认移动构造函数。拷贝赋值运算符:如果没有为类定义拷贝赋值运算符,并且类没有声明任何成员为 const 或引用类型,编译器将自动提供一个默认拷贝赋值运算符。移动赋值运算符:如果没有为类定义移动赋值运算符,并且类的所有非静态成员都可以移动,编译器将自动提供一个默认移动赋值运算符。析构函数:如果没有为类定义析构函数,编译器将自动提供一个默认析构函数。
使用 default 关键字可以显式告诉编译器为类生成默认的特殊成员函数,即使类中有其他构造函数或赋值运算符定义。这在某些情况下很有用,比如当你需要一个默认构造函数,但类中已经有了其他构造函数时。
示例:
class MyClass {
public:
MyClass() = default; // 显式要求编译器生成默认构造函数
// 其他成员函数和变量
};
delete关键字:
在C++中,delete关键字用于删除成员函数,使其无法被实例化。这通常用于以下几种情况:
禁止拷贝和赋值:如果一个类不应该被拷贝或赋值,可以删除拷贝构造函数和拷贝赋值运算符。禁止移动:如果一个类不应该被移动,可以删除移动构造函数和移动赋值运算符。禁止默认行为:对于某些特殊类,可能需要禁止默认构造函数或析构函数。
使用delete关键字的一些要点:
示例:
class NonCopyable {
public:
NonCopyable() = default;
NonCopyable(const NonCopyable&) = delete; // 删除拷贝构造函数
NonCopyable& operator=(const NonCopyable&) = delete; // 删除拷贝赋值运算符
};
特殊成员的合成行为列表:(红框表示支持但可能会废除的行为)
下面是用户声明某种成员函数后,编译器对其他成员函数的行为。
五、字面值类,成员指针与bind交互 1.字面值类
C++中的字面值类(Literal types)是一种特殊的类类型(可以构造编译器常量的类型),它允许在编译时构造对象,并且可以用于常量表达式。以下是字面值类的一些关键特性:
示例:
#include
//在C++14及之后标准是合法的
class MyLiteral {
public:
// constexpr构造函数
constexpr MyLiteral(int v) : x(v) {}
constexpr void inc()
{
x = x + 1;
}
// constexpr成员函数
constexpr int read() const
{
return x;
}
private:
int x;
};
constexpr int MyFun()
{
MyLiteral lit(10);
lit.inc();
lit.inc();
lit.inc();
return lit.read();
}
int main() {
std::cout << MyFun() << std::endl;
return 0;
}
MyLiteral类有一个constexpr构造函数和一个constexpr成员函数,允许在编译时创建和操作对象。
2.成员指针
在C++中,成员指针是一种特殊的指针类型,它指向类的成员(数据成员或成员函数)。
注意:域操作符子表达式不能加小括号
成员指针的使用:
3.bind交互
使用bind + 成员指针构造可调用对象
示例:使用bind + 成员函数指针
#include
#include
class A {
public:
void func(int x) {
std::cout << "Value: " << x << std::endl;
}
};
int main() {
A obj;
auto ptr_to_member_func = &A::func;
// 使用std::bind创建可调用对象
auto bound_func = std::bind(ptr_to_member_func, &obj, std::placeholders::_1);
// 调用bound_func,相当于调用obj.func(10)
bound_func(10);
}
示例:使用bind + 数据成员指针
#include
#include
class A {
public:
void func(int x) {
std::cout << "Value: " << x << std::endl;
}
int x;
};
int main() {
A obj;
auto ptr_to_member_func = &A::func;
// 使用std::bind创建可调用对象
auto bound_func = std::bind(ptr_to_member_func, &obj, std::placeholders::_1);
// 调用bound_func,相当于调用obj.func(10)
bound_func(10);
A obj1;
auto ptr2 = &A::x;
obj1.*ptr2 = 3;
auto data_mem = std::bind(ptr2, &obj1);
std::cout << data_mem() << std::endl;
}
文章来源:https://blog.csdn.net/zwcslj/article/details/139444267
微信扫描下方的二维码阅读本文
暂无评论内容