C++ 面向对象知识汇总(超详细)
学习交流:0voice · GitHub
1.什么是类?
在C++中,类(Class) 是一种用户定义的数据类型,用来描述具有相同特征和行为的一组对象。类是面向对象编程(OOP)的核心概念,它通过将数据和操作封装在一起,提供了创建复杂程序的工具。
类的基本组成部分:
- 成员变量(Member Variables):也称为属性或数据成员,是类中用来存储对象状态的数据。
- 成员函数(Member Functions):也称为方法,是类中定义的用于操作成员变量或执行操作的函数。
类的声明和定义:
类通常由类声明和类定义两部分组成。
1. 类声明(Class Declaration)
这部分通常位于头文件中,定义了类的接口,包括成员变量和成员函数的声明。语法如下:
class ClassName {
public:// 公有成员函数void function1();private:// 私有成员变量int variable1;
};
public
:表示该部分的成员可以被类外部直接访问。private
:表示该部分的成员只能被类内部的函数访问,外部无法直接访问。
2. 类定义(Class Definition)
类的成员函数通常在类声明之外定义(可以放在.cpp文件中),如下:
#include <iostream>class MyClass {
public:void setValue(int val) {variable = val;}int getValue() {return variable;}private:int variable;
};int main() {MyClass obj; // 创建对象obj.setValue(10); // 设置对象的值std::cout << "Value: " << obj.getValue() << std::endl; // 获取对象的值return 0;
}
类的关键特性:
-
封装(Encapsulation):将数据和操作封装在类中,通过访问控制机制(如
public
、private
)控制外部访问。 -
继承(Inheritance):允许一个类从另一个类派生,继承其成员和方法,支持代码复用和扩展。
-
多态性(Polymorphism):通过继承和虚函数实现不同对象对相同消息作出不同响应。
2.面向对象的程序设计思想是什么?
1. 封装(Encapsulation)
封装是将对象的 数据 和 方法 封装在一个类中,隐藏其内部实现细节,只通过公开的接口(成员函数)来访问和修改数据。封装的目的是提高安全性和可维护性。
- 优势:
- 控制对数据的访问:通过将成员变量设为
private
,确保数据只能通过特定的方法(如getter
和setter
函数)来访问和修改。 - 防止外部干扰:防止外部代码直接修改对象的内部状态,保护对象不受意外的或不合理的修改。
- 控制对数据的访问:通过将成员变量设为
class Student {
private:int age; // 私有数据,不能直接访问public:void setAge(int a) {if (a > 0) {age = a; // 设置合法的值}}int getAge() {return age; // 提供安全的访问方式}
};
在这个例子中,age
是一个私有属性,不能直接在类外部访问,而必须通过公开的 setAge
和 getAge
方法来操作。
2. 继承(Inheritance)
继承允许一个类(子类或派生类)从另一个类(父类或基类)继承属性和方法,子类可以复用父类的代码,或者根据需要对继承的功能进行扩展或修改。
- 优势:
- 代码复用:子类继承父类的属性和方法,避免重复代码。
- 扩展现有类:通过继承,可以在不修改父类代码的情况下,扩展或定制新的功能。
class Animal {
public:void eat() {std::cout << "Eating...\n";}
};class Dog : public Animal { // Dog 类继承自 Animal 类
public:void bark() {std::cout << "Barking...\n";}
};
在这个例子中,Dog
类继承了 Animal
类的 eat
方法,并新增了 bark
方法。Dog
对象可以调用 eat
,因为它从 Animal
类继承了这一功能。
3. 多态(Polymorphism)
多态性允许不同的对象对同一消息作出不同的响应。它通过 函数重载 和 虚函数 实现,主要表现为 编译时多态性 和 运行时多态性。
- 编译时多态性(静态多态性):通过 函数重载 和 运算符重载 实现。
- 运行时多态性(动态多态性):通过 虚函数 和 继承 实现,基类指针或引用可以指向派生类对象,并根据实际对象类型调用相应的派生类方法。
class Animal {
public:virtual void sound() { // 虚函数std::cout << "Animal makes a sound\n";}
};class Dog : public Animal {
public:void sound() override { // 重写基类的虚函数std::cout << "Dog barks\n";}
};class Cat : public Animal {
public:void sound() override {std::cout << "Cat meows\n";}
};int main() {Animal* animal1 = new Dog();Animal* animal2 = new Cat();animal1->sound(); // 输出:Dog barksanimal2->sound(); // 输出:Cat meowsdelete animal1;delete animal2;return 0;
}
在这个例子中,虽然 animal1
和 animal2
都是 Animal
类型的指针,但在调用 sound()
方法时,会根据实际对象类型(Dog
或 Cat
)执行相应的重写方法,这就是多态性的体现。
小结:
- 封装:将数据和方法封装在类中,保护数据不被外部直接修改。
- 继承:允许类从已有类继承属性和方法,促进代码复用和扩展。
- 多态:不同对象可以通过同一个接口(如虚函数)表现出不同的行为。
3.C++中struct和class有什么区别?
在C++中,struct
和 class
都可以用于定义包含成员变量和成员函数的数据类型,它们的功能基本上是相同的。不过,二者的主要区别在于 成员的默认访问权限 和 使用习惯。
1. 默认的访问控制权限(Access Control)
struct:
在 struct
中,成员默认是公有的(public)。这意味着,如果你不显式指定成员的访问权限,它们将自动具有公有访问权限。
struct MyStruct {int x; // 默认是 public
};int main() {MyStruct s;s.x = 10; // 可以直接访问,因为是 publicreturn 0;
}
class:
在 class
中,成员默认是私有的(private)。这意味着,如果不显式指定成员的访问权限,它们默认是私有的,外部无法直接访问。
class MyClass {int x; // 默认是 private
};int main() {MyClass c;// c.x = 10; // 错误,不能直接访问 private 成员return 0;
}
2. 传统使用习惯
-
struct 通常用于表示 简单的数据结构,通常只包含成员变量,没有复杂的成员函数。它的用法更类似于C语言中的
struct
,被视为一个数据包(data bundle)。 -
class 则更常用于定义 复杂的对象,包含数据成员和操作方法。它的使用更符合面向对象编程的思想,如封装、继承和多态。
3. 继承的访问权限
在继承时,struct
和 class
也有一些细微差别。
struct 继承:
struct
中继承的默认访问权限是 public。这意味着,如果你不指定访问控制,派生类会公开继承基类的所有成员。
struct Base {int x;
};struct Derived : Base {// 继承 Base 中的 x,默认是 public 继承
};int main() {Derived d;d.x = 10; // 访问 x 是合法的,因为是 public 继承return 0;
}
class 继承:
class
中继承的默认访问权限是 private。如果不指定继承方式,基类的成员在派生类中默认是私有的,无法在派生类对象中访问。
class Base {int x;
};class Derived : Base {// 默认是 private 继承
};int main() {Derived d;// d.x = 10; // 错误,x 在 Derived 中是 privatereturn 0;
}
4. 常见误区
-
有时人们认为
struct
只能包含数据成员,class
才能包含成员函数,这实际上是不对的。在C++中,struct
和class
都可以包含成员函数,并且支持所有的面向对象特性(如继承和多态)。 -
使用
struct
和class
的选择主要基于编程习惯。struct
通常用来表示简单的、仅包含数据的结构,而class
用来表示更复杂的、带有行为的对象。
5. 小结:
特性 | struct | class |
---|---|---|
默认成员访问权限 | public | private |
默认继承访问权限 | public | private |
用途习惯 | 简单数据结构 | 面向对象编程 |
支持面向对象特性 | 是 | 是 |
总结来说,struct
和 class
的功能几乎是相同的,主要区别在于默认的访问控制权限。可以根据需要选择使用 struct
或 class
,但 class
更常用于复杂对象,struct
更适合用于简单数据结构。
4.动态多态有什么作用?有哪些必要条件?
动态多态是面向对象编程(OOP)中非常重要的概念,它允许对象在运行时根据它们的实际类型来选择合适的函数进行调用。这种能力使得程序更灵活、更易扩展,可以处理不同对象的多种行为,而无需在编译时确定具体的对象类型。
动态多态的作用:
-
提高代码灵活性和可扩展性: 动态多态允许程序对不同的对象使用相同的接口。这样,在不修改现有代码的情况下,可以为新类型的对象定义新行为,而不用改动调用这些接口的代码。
-
实现通用接口: 使用基类定义通用接口,派生类可以根据自己的特性实现这些接口。调用者无需知道对象的具体类型,只需要依赖基类接口,这使得代码易于维护和扩展。
-
减少重复代码: 动态多态通过继承和虚函数机制,让派生类能够复用基类的公共代码,同时在需要时定义自己的特殊行为,减少了代码重复。
动态多态的必要条件:
在C++中,要实现动态多态,必须满足以下三个条件:
-
继承(Inheritance): 动态多态依赖于类的继承机制。通常,基类提供一个接口,派生类继承基类并实现或重写基类中的方法。
-
虚函数(Virtual Functions): 基类中的函数必须声明为虚函数(
virtual
),以便在运行时通过基类指针或引用调用派生类的函数。虚函数允许C++的运行时多态性,即程序在运行时根据对象的实际类型来决定调用哪个函数。class Base { public:virtual void show() {std::cout << "Base class" << std::endl;} };class Derived : public Base { public:void show() override { // 重写基类的虚函数std::cout << "Derived class" << std::endl;} };
-
通过基类指针或引用访问对象: 动态多态的核心是在运行时通过基类指针或引用来调用派生类的函数。C++的虚函数表(vtable)机制确保在运行时找到正确的函数实现。
int main() {Base* bPtr;Derived d;bPtr = &d; // 基类指针指向派生类对象bPtr->show(); // 调用的是 Derived 类的 show() 函数,输出 "Derived class"return 0; }
动态多态的实现流程:
- 当通过基类指针或引用调用虚函数时,C++ 会在运行时通过虚函数表找到对象的实际类型,并调用该类型对应的函数实现。
- 如果派生类重写了基类的虚函数,则调用派生类的实现;如果没有重写,则调用基类的实现。
动态多态与静态多态的区别:
- 动态多态:是在运行时根据对象的类型决定调用哪个函数(通过虚函数实现)。动态多态使用继承和虚函数,支持对象的行为多样性。
- 静态多态:是在编译时确定函数调用,通常通过函数重载或模板实现。它不依赖继承和虚函数,而是在编译时进行解析。
总结:
动态多态通过虚函数和继承机制,让程序在运行时能够根据对象的实际类型选择合适的函数执行,从而提高代码的灵活性和扩展性。要实现动态多态,需要满足以下三个条件:
- 类的继承。
- 基类的函数必须是虚函数。
- 通过基类指针或引用访问派生类对象。
5.构造函数为什么不能是虚函数
在C++中,基类的构造函数不能被定义为虚函数,原因有两个:
- 构造函数的目的是初始化对象。当我们创建一个对象时,构造函数被调用来初始化对象的数据成员。在这个阶段,对象才刚刚开始被构建,还没有完全形成,因此它还不具备执行虚函数调用的条件(即,动态绑定)。因为执行虚函数调用需要通过对象的虚函数表指针,而这个指针在构造函数执行完毕后才会被设置。
-
虚函数通常在有继承关系的类中使用,用于实现多态。在子类对象的构造过程中,首先会调用基类的构造函数,然后才是子类的构造函数。如果基类的构造函数被定义为虚函数,那么在执行基类的构造函数时,由于子类的部分还没有被构造,所以无法正确地执行子类构造函数中对虚函数的重写。这就破坏了虚函数的目的,即允许子类重写基类的行为。
因此,基于以上原因,C++不允许构造函数为虚函数。但是,析构函数可以(并且通常应该)被声明为虚函数,以确保当删除一个指向派生类对象的基类指针时,派生类的析构函数能被正确调用,避免资源泄露。
6.为什么基类的析构函数需要定义为虚函数?
在C++中,基类的析构函数通常应该定义为虚函数,尤其是在使用继承和多态时。这样做的主要原因是为了确保正确调用派生类的析构函数,避免资源泄漏和未定义行为。
问题背景
当我们通过基类指针或基类引用指向一个派生类对象时,如果析构函数不是虚函数,销毁对象时可能会发生问题。举个例子:
class Base {
public:~Base() {std::cout << "Base destructor called" << std::endl;}
};class Derived : public Base {
public:~Derived() {std::cout << "Derived destructor called" << std::endl;}
};int main() {Base* obj = new Derived(); // 基类指针指向派生类对象delete obj; // 销毁对象return 0;
}
输出:
Base destructor called
在这个例子中,delete obj
时,只调用了基类 Base
的析构函数,并没有调用 Derived
类的析构函数。这就导致了派生类的资源没有被正确释放,从而可能引发内存泄漏或其他资源管理问题。因为 delete
只会调用基类的析构函数,派生类部分的对象并没有被正确销毁。
虚析构函数的必要性
要解决上述问题,需要将基类的析构函数声明为虚函数(virtual
),这样在通过基类指针或引用销毁派生类对象时,C++ 的虚函数机制会确保派生类的析构函数也能被正确调用。
虚析构函数的实现:
class Base {
public:virtual ~Base() {std::cout << "Base destructor called" << std::endl;}
};class Derived : public Base {
public:~Derived() {std::cout << "Derived destructor called" << std::endl;}
};int main() {Base* obj = new Derived(); // 基类指针指向派生类对象delete obj; // 销毁对象return 0;
}
输出:
Derived destructor called Base destructor called
在这个例子中,基类的析构函数被定义为虚函数,结果 delete
操作时,首先调用了派生类 Derived
的析构函数,然后调用基类 Base
的析构函数,确保了对象的正确销毁。这保证了派生类对象的所有资源,包括其特有的成员,都得到了正确的释放。
详细说明
-
虚函数表(vtable):当基类的析构函数被声明为虚函数时,C++编译器会为类创建一个虚函数表(vtable),这个表用于在运行时找到正确的析构函数。
delete
操作会通过虚函数表找到派生类的析构函数,并依次调用析构函数链,从派生类到基类逐层销毁对象。 -
避免资源泄漏:如果基类的析构函数不是虚函数,当基类指针或引用指向派生类对象时,销毁对象只会调用基类的析构函数,派生类的析构函数不会被执行,从而导致派生类中的资源(如动态分配的内存、文件句柄等)无法被释放,进而导致资源泄漏。
带有资源管理的派生类
class Base {
public:virtual ~Base() {std::cout << "Base destructor called" << std::endl;}
};class Derived : public Base {
private:int* data;
public:Derived() {data = new int[10]; // 动态分配内存std::cout << "Derived constructor: allocating memory" << std::endl;}~Derived() {delete[] data; // 释放内存std::cout << "Derived destructor: releasing memory" << std::endl;}
};int main() {Base* obj = new Derived();delete obj; // 销毁对象,正确调用析构函数链return 0;
}
输出:
Derived constructor: allocating memory Derived destructor: releasing memory Base destructor called
在这个例子中,Derived
类动态分配了内存。如果基类的析构函数不是虚函数,那么在销毁对象时,Derived
的析构函数将不会被调用,导致动态分配的内存无法释放。而通过将基类的析构函数声明为虚函数,C++会确保派生类的析构函数被正确调用,从而释放所有资源。
什么时候不需要虚析构函数?
如果一个基类永远不会作为指针或引用来操作派生类对象,或者你确定它不会被继承,那么就不需要将析构函数声明为虚函数。例如:
- 没有继承关系的类:如果类不是基类或者不会被继承,那么没有必要将析构函数设为虚函数。
- 无需动态分配和多态的简单基类:如果一个基类永远不会被作为指针或引用使用,且派生类对象不会通过基类指针销毁,那么可以不使用虚析构函数。
总结
基类的析构函数定义为虚函数的主要原因是为了确保当通过基类指针或引用销毁派生类对象时,派生类的析构函数也能被正确调用,防止资源泄漏。虚析构函数的必要条件通常出现在使用多态和动态内存分配时。
- 必须使用虚函数的情况:当你需要通过基类指针或引用销毁对象时,基类的析构函数必须是虚函数。
- 虚函数的作用:保证派生类的析构函数被调用,正确销毁派生类对象,释放所有资源。
7.多继承存在什么问题?如何消除多继承中的二义性?
在C++中,多继承允许一个类同时继承多个基类,这样可以使类获得多个基类的属性和行为。然而,多继承带来了几个潜在的问题,最常见的是二义性问题和菱形继承问题。这些问题的存在会影响代码的可读性、维护性和正确性。
1. 二义性问题(Ambiguity Problem)
当一个派生类从多个基类继承,且这些基类中存在同名的成员函数或成员变量时,C++编译器会无法判断该调用哪个基类的成员,导致二义性。
#include <iostream>
using namespace std;class Base1 {
public:void show() {cout << "Base1 show()" << endl;}
};class Base2 {
public:void show() {cout << "Base2 show()" << endl;}
};class Derived : public Base1, public Base2 {// Derived 类从 Base1 和 Base2 都继承了 show() 函数
};int main() {Derived d;d.show(); // 二义性错误:编译器无法确定调用 Base1::show() 还是 Base2::show()return 0;
}
问题解释: 在这个例子中,Derived
类从 Base1
和 Base2
继承了同名的 show()
函数。编译器在遇到 d.show()
时无法确定应该调用 Base1
的 show()
还是 Base2
的 show()
,因此会报二义性错误。
解决方法:显式指定基类
为了解决这个问题,必须通过作用域解析运算符显式指定要调用的基类成员。
int main() {Derived d;d.Base1::show(); // 调用 Base1::show()d.Base2::show(); // 调用 Base2::show()return 0;
}
输出:
Base1 show()
Base2 show()
通过在调用时使用基类名加上作用域解析运算符 ::
,我们可以明确告诉编译器希望调用哪个基类的成员函数,从而消除二义性。
2. 菱形继承问题(Diamond Problem)
菱形继承(又称钻石继承)是多继承中最典型的问题之一。它的结构类似于一个菱形:两个类继承自同一个基类,然后另一个派生类同时继承这两个子类。这样会导致重复继承,即派生类将有两份基类的拷贝。
菱形继承问题
#include <iostream>
using namespace std;class Base {
public:int data;Base() : data(0) {}
};class Derived1 : public Base {
};class Derived2 : public Base {
};class FinalDerived : public Derived1, public Derived2 {
};int main() {FinalDerived fd;// fd.data; // 这会导致编译器报错,无法确定是 Derived1::Base 还是 Derived2::Base 中的 datareturn 0;
}
问题解释: FinalDerived
类通过 Derived1
和 Derived2
都继承了 Base
类,因此 FinalDerived
类有两份 Base
类的拷贝。当我们访问 fd.data
时,编译器无法判断应该访问 Derived1::Base
还是 Derived2::Base
中的 data
,从而导致二义性。
解决方法:虚继承(Virtual Inheritance)
C++ 通过虚继承解决了菱形继承中的重复继承问题。虚继承确保只有一份基类的实例被继承,从而消除重复拷贝。
#include <iostream>
using namespace std;class Base {
public:int data;Base() : data(0) {}
};class Derived1 : public virtual Base { // 使用虚继承
};class Derived2 : public virtual Base { // 使用虚继承
};class FinalDerived : public Derived1, public Derived2 {
};int main() {FinalDerived fd;fd.data = 100; // 现在只有一份 Base::datacout << fd.data << endl; // 输出 100return 0;
}
输出:
100
通过将 Derived1
和 Derived2
从 Base
进行虚继承,我们确保了 FinalDerived
类只有一份 Base
的成员变量 data
,从而解决了菱形继承带来的二义性问题。
3. 二义性问题的其他解决方案
除了显式指定基类和虚继承外,还有其他一些解决多继承中二义性的方法:
(1) 避免多继承
如果可以通过设计优化避免多继承,通常是最好的选择。比如,可以通过组合(composition)或接口来代替多继承。这种方式能让设计更加清晰,避免多继承带来的复杂性。
(2) 使用接口类(抽象类)
在某些场景下,使用接口类(只包含纯虚函数的类)可以解决二义性问题。这种方式类似于Java中的接口,允许类实现多个接口,而不会带来二义性问题。
class Interface1 {
public:virtual void show() = 0; // 纯虚函数
};class Interface2 {
public:virtual void show() = 0; // 纯虚函数
};class Derived : public Interface1, public Interface2 {
public:void show() override {cout << "Derived show()" << endl;}
};int main() {Derived d;d.show(); // 二义性问题不存在,因为 Derived 类重写了所有纯虚函数return 0;
}
在这个例子中,虽然 Interface1
和 Interface2
中都有 show()
函数,但由于它们是纯虚函数,Derived
类必须实现 show()
,因此不会产生二义性。
4. 总结
-
多继承的潜在问题:
- 二义性问题:当多个基类有同名成员时,派生类无法明确调用哪个基类的成员。
- 菱形继承问题:多个派生类从同一个基类继承,导致最终派生类拥有多个基类的实例。
-
解决方案:
- 显式指定基类:通过作用域解析符明确指出要调用哪个基类的成员。
- 虚继承:通过虚继承避免菱形继承导致的重复基类实例。
- 组合和接口:使用组合(composition)或接口(抽象类)设计模式来替代多继承,简化设计,避免复杂的继承结构。
8.拷贝构造函数和赋值运算符重载
拷贝构造函数和赋值运算符重载在C++中都是用于处理对象的复制,但它们的使用场景和行为有所不同。让我们详细分析这两者的定义、区别以及它们各自的应用场景。
1. 拷贝构造函数(Copy Constructor)
拷贝构造函数用于创建对象时的初始化,即用一个已经存在的对象来初始化新创建的对象。它的调用发生在对象创建的同时,用另一个对象作为副本进行初始化。
定义:
class MyClass {
public:MyClass(const MyClass& other); // 拷贝构造函数
};
触发拷贝构造函数的场景:
- 当一个对象直接初始化另一个对象时,例如:
MyClass obj1; MyClass obj2 = obj1; // 调用拷贝构造函数
- 将对象作为参数传递给函数时(按值传递):
void func(MyClass obj); // 传递对象时调用拷贝构造函数
- 当函数返回一个对象(按值返回)时:
MyClass func() {MyClass temp;return temp; // 返回对象时调用拷贝构造函数 }
2. 赋值运算符重载(Assignment Operator Overload)
赋值运算符重载用于将一个已经存在的对象的值赋给另一个已经存在的对象。它发生在两个对象都已经创建之后,进行值的赋值操作。
定义:
class MyClass {
public:MyClass& operator=(const MyClass& other); // 赋值运算符重载
};
触发赋值运算符重载的场景:
- 当一个已经存在的对象被赋值为另一个对象时,例如:
MyClass obj1; MyClass obj2; obj2 = obj1; // 调用赋值运算符
- 赋值发生时,目标对象必须已经存在。
3. 区别总结
比较点 | 拷贝构造函数 | 赋值运算符重载 |
---|---|---|
目的 | 用一个已有对象创建新对象 | 将一个已有对象的内容赋值给另一个已有对象 |
调用时机 | 对象创建时,用另一个对象初始化 | 对象已经存在,然后将另一个对象的值赋给它 |
函数签名 | MyClass(const MyClass& other) | MyClass& operator=(const MyClass& other) |
内存分配 | 可能会分配新内存给新对象 | 通常不会分配新内存(除非需要深拷贝) |
默认行为 | 逐成员拷贝(浅拷贝) | 逐成员赋值(浅拷贝) |
操作对象的数量 | 创建一个新的对象 | 操作两个已经存在的对象 |
自引用 | 不涉及自引用 | 可能需要检查自引用 |
返回类型 | 无返回值 | 返回对当前对象的引用(*this ) |
4. 默认实现 vs 自定义实现
C++ 自动为每个类提供默认的拷贝构造函数和赋值运算符,但它们都是执行浅拷贝,即逐成员的复制或赋值。如果类中包含动态分配的内存或其他需要深度管理的资源,必须自定义拷贝构造函数和赋值运算符,以确保正确处理这些资源。
如果类包含动态内存分配,使用默认的拷贝和赋值可能会导致资源管理问题(如重复释放内存、悬空指针等)。
5. 深拷贝示例
对于包含动态内存的类,需要自定义拷贝构造函数和赋值运算符以执行深拷贝,确保在复制对象时分配独立的内存,而不是简单复制指针。
class MyClass {
private:int* data; // 动态分配的资源
public:// 构造函数MyClass(int value) : data(new int(value)) {}// 拷贝构造函数(深拷贝)MyClass(const MyClass& other) : data(new int(*other.data)) {std::cout << "Copy constructor called" << std::endl;}// 赋值运算符重载(深拷贝)MyClass& operator=(const MyClass& other) {std::cout << "Assignment operator called" << std::endl;if (this == &other) // 防止自我赋值return *this;// 先释放当前对象的资源delete data;// 分配新的资源并复制数据data = new int(*other.data);return *this;}// 析构函数~MyClass() {delete data;}// 打印数据void print() const {std::cout << "Value: " << *data << std::endl;}
};int main() {MyClass obj1(10);MyClass obj2 = obj1; // 调用拷贝构造函数obj2.print();MyClass obj3(20);obj3 = obj1; // 调用赋值运算符obj3.print();return 0;
}
输出:
Copy constructor called
Value: 10
Assignment operator called
Value: 10
6. 自我赋值问题
在实现赋值运算符时,必须处理自我赋值(self-assignment)的情况。自我赋值指的是对象将自己赋值给自己,例如 obj = obj;
。如果不处理这种情况,可能导致内存泄漏或其他不良行为。
在赋值运算符的实现中,一般通过检查 this
指针来避免自我赋值:
if (this == &other) {return *this;
}
7. 总结
- 拷贝构造函数用于在创建对象时通过另一个对象进行初始化。
- 赋值运算符重载用于将一个对象的值赋给另一个已存在的对象。
- 二者的最大区别在于调用时机,前者是在创建新对象时,后者是在赋值时。
- 如果类涉及动态资源管理(如动态内存分配),需要自定义深拷贝的拷贝构造函数和赋值运算符,并处理自我赋值问题。
9.类型转换分为哪几种?各自有什么样的特点?
在C++中,类型转换(Type Casting)分为隐式类型转换和显式类型转换两大类。显式类型转换还可以通过C风格的强制转换或C++提供的四种类型转换操作符来实现。
1. 隐式类型转换(Implicit Type Conversion)
隐式类型转换又称为自动类型转换,是指编译器自动将一种数据类型转换为另一种兼容类型,不需要程序员明确地写出转换语句。
特点:
- 自动进行:不需要程序员干预,编译器会根据需要自动转换类型。
- 类型兼容性:通常发生在兼容类型之间,比如从低精度类型向高精度类型转换(如
int
转换为double
),或从窄类型转换为宽类型(如char
转换为int
)。 - 数据可能丢失:在某些情况下可能导致数据精度丢失,比如将
double
转换为int
时,小数部分会被截断。
int x = 10;
double y = x; // 隐式类型转换:int 转换为 double
在这个例子中,int
类型的 x
自动转换为了 double
类型,并赋值给 y
。
2. 显式类型转换(Explicit Type Conversion)
显式类型转换,也叫强制类型转换,要求程序员明确地指定数据类型转换,常见的方式有C风格的强制转换和C++的四种类型转换操作符。
2.1 C风格强制转换(C-style Cast)
C风格的类型转换语法类似于函数调用,通过在类型前加上目标类型的括号实现强制转换。
特点:
- 语法简单:C风格类型转换语法简单。
- 不安全:由于它忽略了类型安全检查,容易引发问题,尤其在复杂对象和指针的转换中。
int a = 10;
double b = (double)a; // C风格的强制转换
2.2 C++类型转换操作符
C++引入了四种类型转换操作符,提供了更严格和明确的类型转换机制,主要包括:
static_cast
:用于大多数标准转换dynamic_cast
:用于安全地向下转换多态类型const_cast
:用于移除或添加const
属性reinterpret_cast
:用于低级别、危险的指针转换
2.2.1 static_cast
static_cast
是最常用的类型转换操作符,用于执行任何可以在编译时检查的转换。适用于基本数据类型之间的转换、指针类型之间的转换(前提是指针类型兼容)等。
特点:
- 编译时转换:在编译阶段进行转换,编译器会进行类型检查。
- 安全性较高:不会像
reinterpret_cast
那样进行完全不同类型之间的转换,确保类型转换是合法的。
int a = 10;
double b = static_cast<double>(a); // 用 static_cast 进行类型转换
2.2.2 dynamic_cast
dynamic_cast
用于在继承体系中进行安全的向下转换(downcasting)。它要求基类中至少有一个虚函数(通常是虚析构函数),以确保对象具有多态性。
特点:
- 运行时检查:
dynamic_cast
会在运行时检查转换是否安全。如果转换失败,指针会返回nullptr
,引用会抛出std::bad_cast
异常。 - 适用于多态类型:只能用于具有多态性的类,即类中包含虚函数。
class Base {
public:virtual void show() {}
};class Derived : public Base {
public:void show() override {}
};Base* basePtr = new Derived();
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr); // 安全的向下转换
if (derivedPtr) {derivedPtr->show();
} else {std::cout << "Conversion failed!" << std::endl;
}
2.2.3 const_cast
const_cast
用于移除或添加const
限定,常用于对常量对象进行修改或对非const
指针进行转换。
特点:
- 仅限于
const
属性的移除或添加:不能用于其他类型的转换,只能用于指针或引用的const
属性操作。 - 不改变底层类型:它不会改变对象的底层类型,只是改变了对象的
const
属性。
const int a = 10;
int* p = const_cast<int*>(&a); // 移除 const 限定
*p = 20; // 可能引发未定义行为
2.2.4 reinterpret_cast
reinterpret_cast
是一种非常底层的强制类型转换,用于将一种类型的指针转换为另一种不相关类型的指针,或者转换为整数。它允许进行位级别的转换,因此是最不安全的转换方式。
特点:
- 危险且不安全:
reinterpret_cast
可以将不相关的指针类型相互转换,编译器不进行类型安全检查,容易导致运行时错误。 - 用于低级别转换:通常在需要进行底层数据操作时使用。
int a = 42;
int* p = &a;
char* cp = reinterpret_cast<char*>(p); // 将 int* 转换为 char*
3. 各类类型转换的特点总结
类型转换方式 | 特点 | 使用场景 |
---|---|---|
隐式类型转换 | 编译器自动进行,安全性较高 | 兼容类型之间的自动转换,如 int 转换为 double |
C风格强制转换 | 简单但不安全,容易产生问题 | 不推荐在C++中使用 |
static_cast | 编译时转换,适用于大部分标准转换,安全性较高 | 基本类型之间的转换、兼容指针类型之间的转换 |
dynamic_cast | 运行时检查,多用于继承体系中的向下转换,依赖多态性 | 需要安全的向下转换多态类时 |
const_cast | 用于移除或添加const 限定 | 需要修改const 对象或指针的const 属性时 |
reinterpret_cast | 最危险的类型转换,允许进行不同类型的强制转换,编译器不检查 | 低级别数据操作,进行位级别转换时 |
4. 总结
- 隐式类型转换:编译器自动进行,常见于兼容类型之间。
- C风格强制转换:语法简单但不推荐使用,C++提供了更安全的类型转换操作符。
static_cast
:适用于标准类型转换,安全性较高。dynamic_cast
:适用于继承体系中的多态转换,确保安全性。const_cast
:用于const
属性的移除或添加。reinterpret_cast
:危险的底层转换,通常在位级别操作中使用。
相关文章:
C++ 面向对象知识汇总(超详细)
学习交流:0voice GitHub 1.什么是类? 在C中,类(Class) 是一种用户定义的数据类型,用来描述具有相同特征和行为的一组对象。类是面向对象编程(OOP)的核心概念,它通过将…...
stm32使用SIM900A模块实现MQTT对接远程服务器
SIM900A模块是一种GSM/GPRS无线通信模块,它可以通过SIM卡连接移动通信网络,并通过串口或USB接口与微控制器或计算机进行通信。 SIM900A驱动代码如下: #include "stm32f10x.h" #include "stdio.h" #include "stdlib.h" #include "sim900a…...
MATLAB Simulink (一)直接序列扩频通信系统
MATLAB & Simulink (一)直接序列扩频通信系统 写在前面1 系统原理1.1 扩频通信系统理论基础1.1.1 基本原理1.1.2 扩频通信系统处理增益和干扰容限1.1.3 各种干扰模式下抗干扰性能 1.2 直接序列扩频通信系统理论基础1.2.1 基本原理1.2.2 物理模型 2 方…...
标准数字隔离器主要特性和应用---腾恩科技
在现代电子系统中,不同电路部分之间需要可靠的隔离,尤其是在高压环境或必须保持敏感信号完整性的情况下。一种这样的解决方案是使用标准数字隔离器。这些组件在电路的不同部分之间提供电气隔离,确保安全、降噪和可靠的信号传输。本文深入探讨…...
Spring事务的七种传播行为
Spring事务的七种传播行为 1.事务的传播行为是什么?2.具体传播行为2.1 REQUIRED ,默认,存在事务则加入该事务,不存在则新建一个事务2.2 REQUIRES_NEW,每次新开启事务,新老事务相互独立2.3 NESTED࿰…...
win10怎么卸载软件干净?电脑彻底删除软件的方法介绍,一键清理卸载残留!
电脑上经常会下载各种各样的软件来协助我们办公,不同的软件能够满足不同的需求。 但是不少软件可能使用频率没有那么高,甚至完全不使用。这个时候就需要将这些不常用的电脑软件卸载掉了,卸载软件能够释放一定的存储空间,提高电脑…...
excel中,将时间戳(ms或s)转换成yyyy-MM-dd hh:mm.ss或毫秒格式
问题 在一些输出为时间戳的文本中,按照某种格式显示更便于查看。 如下,第一列为时间戳(s),第二列是转换后的格式。 解决方案: 在公式输入框中输入:yyyy/mm/dd hh:mm:ss TEXT((A18*3600)/8640070*36519, "yyy…...
机房巡检机器人有哪些功能和作用
随着数据量的爆炸式增长和业务的不断拓展,数据中心面临诸多挑战。一方面,设备数量庞大且复杂,数据中心内服务器、存储设备、网络设备等遍布,这些设备需时刻保持良好运行状态,因为任何一个环节出现问题都可能带来严重后…...
Redis Search系列 - 第一讲 创建索引
目录 一、引言二、全文检索基本概念三、创建索引 一、引言 Redis Search 是 Redis 的一个模块,用于提供全文搜索和二级索引功能。它允许在 Redis 数据库中执行复杂的搜索查询,并支持多种数据类型和查询操作。以下是 Redis Search 的一些关键特性&#x…...
bat 重置 Navicat 试用
bat 脚本文件 echo off set dnInfo set dn2ShellFolder set rpHKEY_CURRENT_USER\Software\Classes\CLSID :: reg delete HKEY_CURRENT_USER\Software\PremiumSoft\NavicatPremium\Registration14XCS /f %针对<strong><font color"#FF0000">navicat<…...
【真题笔记】09-12年系统架构设计师要点总结
【真题笔记】09-12年系统架构设计师要点总结 41 视图DSSA(特定领域架构)集成系统数据库管理设计模式操作符运算符综合布线备份数据库集成工作流技术软件质量保证需求管理需求开发结构化方法企业战略数据模型事务数据库主题数据库系统设计原型开发静态分析…...
Node + HTML搭建自己的ChatGPT [基础版]
文章目录 明明外面的ChatGPT产品那么多了,为什么要在本地搭建自己的ChatGPT呢?整体架构流程1. 获取APIKey1.1 常见的AI模型1.2 为什么选DeepSeek1.3 怎么获取DeepSeek的APIKey1.3.1 注册并登录DeepSeek开放平台1.3.2 选择API keys1.3.3 创建API key1.3.4…...
关于小程序审核需要提交订单列表页面path的修改办法
小程序又又又又又搞事情啦~~~ 从12月31号起,所有有订单生成逻辑的小程序在审核过程中,必须要填写订单列表页面的path才可以进行审核 在代码层面上会有一些小的改动,下面就告诉大家怎么去修改吧。 第一步…...
使用 Nginx 在同一端口部署两个前端项目并配置子路径
在现代 Web 开发中,我们经常需要在同一台服务器上部署多个前端项目。这不仅可以节省资源,还可以简化管理。本文将指导你如何使用Nginx在同一端口上部署两个前端项目,并通过配置子路径来区分它们。 环境准备 首先,我们需要准备两…...
怎么选择独立站SEO效果好的wordpress模板
选择独立站SEO效果好的WordPress模板需要考虑多个因素,包括模板的代码质量、加载速度、SEO友好性以及与SEO插件的兼容性。以下是一些具体的建议: 1. 代码简洁:选择代码简洁的WordPress主题,因为干净的代码不仅使网站更加安全可靠…...
深度学习速通系列:超长法律文件隐私过滤(基于预训练模型Bert)
法律文件隐私过滤 网上使用bert的中文模型进行命名识别教程少的可怜,摸索了一周的时间,硬是把法律文书的人名全部识别出来了,目前可以达到98.9999%(开玩笑的,不过准确率保守估计是有90%以上).注意:这个法律文书目前只是针对裁决书,其他还没测试过,可支持超长文本识别 github仓…...
【数据结构与算法】之队列详解
队列(Queue)是一种重要的线性数据结构,遵循先进先出、后进后出的原则。本文将更详细地介绍队列的概念、特点、Java 实现以及应用场景。 模运算小复习: a % b 的值总是小于b 5 % 4 1 5 % 2 1 1 % 5 1 4 % 5 4 1. 队列…...
python最新h5st4.9.1调用源码(2025-10-25)
废话不多说,直接上源码,需要技术支持的私。 一、调用js方法: # -*- coding: utf-8 -*- """ -------------------------------------------------Author: byc6352File: jdh5st.pyTime: 2024/10/25 08:03Technical Support:by…...
微软投资比特币:将总资产1%投资于BTC?股东投票决定最终结果!
随着比特币及其他加密货币在全球金融市场中的影响力不断增加,科技巨头微软(Microsoft)也开始考虑是否在其资产负债表上纳入比特币。根据近期提交给美国证券交易委员会(SEC)的文件,微软将在2024年12月10日举…...
vue中标签的ref和id的用法和区别优缺点
Vue 3 中 ref 和 id 的用法详解:区别、优缺点及使用场景 在 Vue 3 开发中,我们经常需要获取 DOM 元素或组件实例来进行交互。Vue 提供了 ref 和原生 HTML 属性 id 来实现这种操作。虽然 ref 和 id 都能标识并操作元素,但它们的使用方式、优缺…...
Python基础知识-文件篇
Python 的文件操作是指与文件进行交互的各种技术和方法,包括读取、写入、关闭文件等。以下是对 Python 文件操作的详细介绍: 打开文件 要进行文件操作,首先需要打开文件。Python 提供了内置的 open() 函数。 file open(example.txt, r) …...
MacOS 环境下 VSCode 的 C++ 环境搭建
MacOS 环境下 VSCode 的 C 环境搭建 编译器安装 编译器可以选择 Clang 或者 GCC,在 MacOS 上 Clang 的安装更为简单一些。 Clang(推荐) 打开终端输入命令, clang -v 查看是否已经安装。 如果已经安装,会输出类似于如下的信息࿱…...
WPF样式
WPF(Windows Presentation Foundation)是微软推出的一种用于构建Windows应用程序的UI框架。它提供了一套丰富的控件、图形和动画功能,允许开发者创建具有丰富视觉效果的现代用户界面。WPF中的样式(Styles)是一种强大的…...
Vue Router 如何配置 404 页面?
在 Vue 项目中,如果你想配置一个 404 页面(即找不到页面提示),你需要通过 Vue Router 来设置。这通常通过将路由配置中的 *(通配符)指向一个 404 组件来实现。 // 定义路由部分 const routes [{path: /,c…...
【C++:智能指针】
什么是内存泄漏 内存泄漏是指因为疏忽或者错误造成程序对一部分不再使用的内存没有进行释放的情况,内存释放不是指内存在物理上的消失,而是应用程序分配某段内存时,因设计错误,失去了对该内存的控制,从而造成内存浪费 …...
onlyoffice docker启用jwt并生成jwt
一、说明 本文是docker教程,linux/win的安装版本也类似,只需要修改配置文件中的secrt就可以了【Configuring JWT for ONLYOFFICE Docs - ONLYOFFICE】 二、正文开始 docker启动时候如果不想使用jwt,加上参数-e JWT_ENABLEDfalse就可以了&…...
希尔贝壳受邀参加首届“数据标注产业大会暨供需对接会”
为推动数据标注产业高质量发展,促进数据标注基地快速形成面向产业的规模化服务能力。10月22日,由国家数据局数字科技和基础设施建设司指导的首届“数据标注产业大会暨供需对接会”在北京召开,希尔贝壳受邀参加。 大会旨在进一步推动数据标注…...
35.第二阶段x86游戏实战2-C++遍历技能
免责声明:内容仅供学习参考,请合法利用知识,禁止进行违法犯罪活动! 本次游戏没法给 内容参考于:微尘网络安全 本人写的内容纯属胡编乱造,全都是合成造假,仅仅只是为了娱乐,请不要…...
Jenkins发布vue项目,版本不一致导致build错误
问题一 yarn.lock文件的存在导致在自动化的时候,频频失败问题二 仓库下载的资源与项目资源版本不一致 本地跑好久的一个项目,现在需要部署在Jenkins上面进行自动化打包部署;想着部署后今后可以省下好多时间,遂兴高采烈地去部署&am…...
vue3使用webSocket
1.安装插件 npm i vueuse/core10.11.12.引入使用 import { useWebSocket } from "vueuse/core"const { send, open, close: wsClose, status } useWebSocket(ws://192.168.100.90:53021/inms-application/alarm, {onMessage: (ws, { data }) > {console.log(&q…...
wordpress 幻灯数据库/电商网
在写 Python 代码的时候有时候会遇到变量需要叫 type、class 的情况。但 type 是一个 Python 的 ? built-in function,class 是一个 keyword。 有一种解决方法是在变量后加单下划线得到 type_,代码里也已经有地方在用,符合 PEP8 的规范。参考…...
用python做网站多吗/深圳网站建设系统
ADO.NET Entity Framework 入门示例向导(附Demo程序下载)- 系列2本篇文章在《ADO.NET Entity Framework 入门示例向导(附Demo程序下载)》基础上,进一步演示如何使用EntityClient 新数据提供程序、对象服务(…...
企业做网站域名需要自己申请吗/百度代理查询
C#内存泄漏的事例 一,使用非托管资源忘记及时Dispose (1) 使用完非托管资源一定要Dispose或者使用using using (FileStream fsWrite new FileStream(path, FileMode.OpenOrCreate, FileAccess.Write)){string str "我好像不能全部覆盖源文件中的数据";…...
广东像一起做网店的网站/西安网站公司推广
抄别人的东西。一个一个字符打下来,还是有收获的。 一个相当简单的CSS页面布局。 html代码如下: 代码 <div id"Header"><h1><span>XXXXXXXX</span></h1><ul id"topMenu"><li class"fir…...
wordpress自定义内容管理/天眼查企业查询
Zookeeper的目录整理如下 1. 【分布式】分布式架构 2. 【分布式】一致性协议 3. 【分布式】Chubby与Paxos 4. 【分布式】Zookeeper与Paxos 5. 【分布式】Zookeeper使用--命令行 6. 【分布式】Zookeeper使用--Java API 7. 【分布式】Zookeeper使用--开源客户端 8. 【分布式】Zoo…...
企业网站结构图/舆情分析报告
环境: 联想E14 Win10专业版 钉钉最新版6.5.50-11089104 问题描述: 钉钉在线表格下载后内容空白,在线编辑有时打开闪一下就空白,下载的表格sheet2还是空白的 sheet2有数据 下载后空白: 从下面方式下载,在线编辑进去下载,就卡死无响应,单独下载csv格式是可以的 原因分…...