C++整体
written by :codedrawing
栈区、堆区、全局区、文字常量区、程序代码区
简介
C++是对C的升级和完善;C++诞生用于解决大规模问题、
- 弥补C语言中的缺陷
- 支持面向对象同时也支持面向过程编程
- 严谨+效率+安全
- 支持泛型编程
C语言中,char *ptr=“hello”;是没有问题的,但是对于C++的编译器g++中编译会出问题,因为字符串常量不能修改,但是char *指向的内存可以修改,需要加上const
字符串常量存储在
常量区(只读存储区)const char *ptr=“hello”;
领域:游戏引擎、服务器、算法
C++基础
程序基本结构
#include <iostream>//头文件
//using namespace std;//重命名命名空间
int main()
{
std::cout << "Hello, World!" << std::endl;
return 0;}
输入输出
//cin与cout对于基本类型 可以直接输入与输出
cout<<"c++"<<endl; //end of line
cout<<144<<endl;
cout<<3.634<<endl;
//cerr: 错误输出,没有缓存
严格的类型检查
//对const类型, 返回值类型, 有更严格的类型检查
char *ptr = "test";
//左右两边类型不一致,在c++中, const char * 无法自动转换成char*, 除非强转
int *ptr = malloc(256);
//左右两边类型不一致,在c++中, void * 无法自动转换成int*, 除非强转
函数重载
定义:C++允许同一区域,函数名相同,参数列表不同(参数类型,参数个数)
不能只通过返回值来区分相同的函数
📌cannot overload functions distinguished by return type alone
调用时,如给的参数不一致可以回发生转换
示例
int add(int a,int b)
{
return (a+b);
}
int add(int a,int b,int c)
{
return (a+b+c);
}
float add(float a,float b)
{
return (a+b);
}
//注意:调用时,传入的实参类型与形参不一致,可能会发生类型转换(不安全)
//注意:不能通过返回值进行区分
函数默认参数
C++允许,给参数设置默认值
//注意:在函数的声明中给参数设置默认值
int add(int a=10,int b=20){
return (a+b);
}
但是如果部分参数设置默认值,必须从右到左倒着设置
int add(int a,int b=20){
return (a+b);
}
作用域
-
全局域:在函数外部
-
名字空间域:在名字空间下
namespace aa
{
int i= 11;
void fun(void)
{
printf("fun 1\n");
}
}
namespace bb
{
int i = 22;
void fun(void)
{
printf("fun 2\n");
}
}
使用:
aa::i
bb::fun
-
类域:在结构体中或者是在class中
-
局部域 :在函数内部,或者switch、for、while语句中
内联函数
在编译时直接将代码嵌入到调用处
c++引入inline关键字,用来实现类似于宏函数的效果
避免宏函数的缺陷(宏函数只是替换,不是真正的函数)
inline int max(int a,int b){
return a>b?a:b;
通过空间换时间,所以一般不要太复杂
引用
相当于给变量取别名
int i;
int &r = i;//给变量i设置别名r,对r的任何操作等同于i
//在c++中传参
//相当于有三种方式 1.赋值传递 2.传地址 3.传引用
面向对象
面向对象特点
封装:把数据和操作封装到一个类中,私有化数据
抽象:
继承:
多态:
权限 public private
public:共有权限,类的内部,与外部都可以访问
private :私有权限,仅类的内部可以访问
C++使用 class代替struct时 不加权限标识时默认为private
构造与析构
构造函数
- 定义对象时,自动执行的函数,用于初始化
函数名与类名相同,没有返回值,可以有多个版本
定义函数时,自动执行
析构函数
- 对象销毁时,自动执行的函数,用于结束时的清理工作
函数名与类名相同且前面加上~,没有返回值,没有参数,不可以重载
拷贝构造
-
一种特殊的构造函数(以一个对象为蓝本去构造另一个对象:拷贝构造从来都不是显示调用,而是编译器隐式调用)
-
区分赋值和拷贝
-
赋值:
-
Demo a(123); Demo b(321); b = a; //赋 -
Demo a(123); // Demo b(a); // Demo b = a; 拷贝Demo a;
-
-
Demo b(a); //
-
-
调用方式有三:
Demo a(123); // Demo b(a); // Demo b = a;-
动态创建对象
Demo a; Demo *p = new Demo(a); // -
函数参数传递:对象传递
void test(Demo obj) { } test(obj); -
拷贝:
- 浅拷贝:只是将成员变量的值 拷贝给另一个对象,新构造的对象和原对象指向的是同一片空间
- 深拷贝:对象指针成员变量,指向堆区空间,拷贝时需要重新给新对象开堆区空间
-
this指针
指向当前对象,在类的类部使用。
//用法1:当形参予类中重名
char *ptr=new char [64]; //开辟空间
delete [] ptr; //释放空间
//对于基本类型 ,也可以直接delete ptr;
int *ps=new int[10];
delete [] ps;
struct student{
std::string brand;
std::string capacity;
float remind_cap;
float price;
};
student *student1=new student();
delete student1;
//用法2:
New delete
//C++中用于代替 malloc() 与 free();
int *p1 = new int; //开辟一个int
delete p1; //释放
char *ptr = new char[64]; //开辟64个char
delete [] ptr; //释放空间
int *ps = new int[10];
delete [] ps;
node *ptr_node = new node; //开辟一个node
delete ptr_node; //释放一个node
//注意:对于基本类型释放时直接delete ptr,对于数组空间 delete [] ptr
特殊关键字
static
限制作用域,安全和效率的妥协
static修饰成员变量
—>静态成员变量
- 在类外去定义
- 可以不通过对象来访问,类名::变量名,所有对象共同拥有,只有一份
static修饰成员函数
—>静态方法,静态函数
- 可以通过不用定义对象来调用,用类名::函数();
- 静态函数没有this指针
const
没有修改变了的函数尽量加上const
const修饰对象
class MyClass {
public:
void foo() const {
// 该函数是 const 成员函数,不会修改类成员变量的值
}
int bar() {
// 该函数不是 const 成员函数,可以修改类成员变量的值
return ++m_member;
}
private:
int m_member;
};
int main() {
const MyClass obj1; // const 类型的对象
MyClass obj2; // 非 const 类型的对象
obj1.foo(); // 可以调用 const 成员函数
// obj1.bar(); // 编译错误,无法调用非 const 成员函数
obj2.foo(); // 可以调用 const 成员函数
obj2.bar(); // 可以调用非 const 成员函数
return 0;
}
友员
友员不是本类成员,而是其他的,类似于extern
声明可以在私有,也可以在共有
友员关系不能被继承,因为他不属于该类
运算符重载
函数可以重载,运算符也可以重载
为了能够对自定义对象进行处理
模版
支持参数化多态的工具,就是让类或者函数声明为一种通用类型
模版的声明或定义只能在全部、命名空间或者类范围内进行。即不能在局部范围、函数内部进行。
C++模板(Templates)是 C++ 语言中的一种高级特性,它允许编程者编写泛型代码,从而提高代码的重用性和灵活性。模板的主要用途包括:
- 泛型编程:模板使您能够编写独立于数据类型的通用代码。使用模板,您可以定义一个通用结构(如类或函数),然后在实例化时指定其所需的特定数据类型。这样就避免了编写针对每个数据类型的重复代码。
- 类型安全:使用模板,您可以在编译时检查类型错误,从而提高代码的安全性。因为模板根据所需类型自动生成代码,所以不会出现将错误类型传递给函数的问题。
- 容器类和算法:C++标准库中的许多容器类(如vector、list、map等)和算法(如sort、find等)都是基于模板的,因此它们可以在不同类型的数据上使用。这使得编写能够处理各种数据类型的代码更加简便。
- 性能优化:通过使用模板,编译器可以在编译时生成针对特定数据类型的代码,从而提高运行时性能。模板是编译时生成代码的,因此它们不会引入额外的运行时开销。
总之,C++模板提供了一种实现泛型编程的强大工具,使开发人员能够编写灵活、可重用的代码,同时保持类型安全和性能优势。
组合
在一个对象里面有另一个对象作为成员
继承
当父类中手写了构造方法时但没有写无参构造时,子类必须要手动调用父类的构造方法,否则会报错;因为子类构造时首先需要构造父类,但是只要手动写了构造方法,就没有默认的方法了,所以找不到无参构造,就会报错
class SubDemo;
class Demo{
private :
int x;
public :
Demo(){}//如果没写无参,那么需要再下面改成注释的语句
Demo(int x):x(x){}
virtual ~Demo(){}
operator SubDemo();
int getX ()const{
return x;
}
};
class SubDemo:public Demo{
private :
int y;
public :
SubDemo(int y):y(y){}
// 这里
SubDemo(int y):Demo(y),y(y){}
/*
当父类中手写了构造方法时但没有写无参构造时,子类必须要手动调用父类的构造方法,否则会报错;因为子类构造时首先需要构造父类,但是只要手动写了构造方法,就没有默认的方法了,所以找不到无参构造,就会报错
*/
virtual ~SubDemo(){}
int getY ()const{
return y;
}
};
Demo:: operator SubDemo(){
return x;
}
扩展已经存在的代码,实现代码重用
保护的属性,不能直接访问,必须通过public中的属性去访问保护(类似于private保护的属性)
子类访问
可以通过子类的public中的属性去访问父类中的protected属性
共有继承
- 派生类继承基类的公有、保护、私有属性,除了虚函数、构造以及析构函数
- 在类的外部:通过派生类对象访问共有成员(因为继承过来的权限、属性、行为不变)
- 在类的内部:通过派生类成员函数可以访问基类的公有成员和保护成员
- 私有成员不管在派生的内部外部都不能访问,只有父类自己才能访问
保护继承
- 派生类继承基类的公有、保护、私有属性,除了虚函数、构造以及析构函数
- 原有的公有成员会变成保护成员
- 不能通过派生类的外部对象访问基类的公有成员,只能通过基类内部访问
- 私有成员依然私有
私有继承
- 派生类继承基类的公有、保护、私有属性,除了虚函数、构造以及析构函数
- 派生类继承所有的属性都变成私有,只能通过son内部访问
- 如果派生类继续派生,那么原来继承于基类的所有成员都是该派生类私有属性,所以该派生类派生的新派生类都无法直接访问
构造和析构的顺序
构造顺序:先基类再派生类(如果涉及多次派生或者多个不同类对象,那么就看谁先实例化)
析构顺序:先派生类再基类(看生命周期)
| 访问权限 | 基类内部 | 基类派生类内部 | 外部 |
|---|---|---|---|
| 公有继承 | 可访问 | 可访问 | 可访问 |
| 私有继承 | 不可访问 | 不可访问 | 不可访问 |
| 保护继承 | 不可访问 | 可访问 | 不可访问 |
| 派生类友元 | 可访问 | 可访问 | 可访问 |
| 其他非派生类 | 不可访问 | 不可访问 | 可访问 |
向下/上隐式转换
向上转换是一种从高精度向低精度进行的转换
在C++中,子类(派生类)和父类(基类)之间的转换有两种类型:向上转换(upcasting)和向下转换(downcasting)。
向上转换是从派生类转换为基类,这种转换是安全的,因为派生类对象可以看作是基类对象的一种特例。向上转换通常是隐式的,不需要显式地进行类型转换。将派生类对象的基类部分复制给基类对象,派生类舍弃自己的独特的数据
向下转换是从基类转换为派生类,这种转换可能导致问题,因为基类对象可能没有派生类所需的所有属性和方法。在C++中,向下转换是不安全的,需要显式地进行类型转换。
为了安全地执行向下转换,我们可以使用C++中的dynamic_cast关键字。dynamic_cast在运行时检查对象类型,如果转换是安全的,它会执行转换,否则它会返回空指针。
这里用到了下面的转换函数 dynamic_cast,可以再下面看详细介绍
#include <iostream>
class Base {
public:
virtual ~Base() {}
};
class Derived : public Base {
public:
void derivedMethod() {
std::cout << "Derived method called!" << std::endl;
}
};
int main() {
Base* basePtr = new Derived(); // 向上转换,隐式的
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr); // 向下转换,显式的
if (derivedPtr) {
derivedPtr->derivedMethod(); // 调用派生类的方法
} else {
std::cout << "Downcasting failed!" << std::endl;
}
delete basePtr;
return 0;
}
/*
在这个例子中,我们首先创建了一个Derived类的对象,并将其指针赋值给Base类指针,这是一个向上转换。然后我们尝试使用dynamic_cast将Base类指针转换为Derived类指针,这是一个向下转换。如果向下转换成功,我们调用派生类的方法,否则输出错误信息。
注意:为了使dynamic_cast正常工作,基类中需要至少有一个虚函数,这样它才具有多态特性。在上面的示例中,为Base类定义了一个虚析构函数。
*/
虚基类
如果一个派生类通过多条路径(多个基类)继承同一个虚基类,则需要使用虚基类来避免多次继承造成的数据冗余和二义性问题。虚基类是一种特殊的基类,它只在继承关系中出现一次,并且由最终的派生类负责初始化。
在继承列表中使用关键字 virtual
class B : virtual public A {
public:
int b;
};
class C : virtual public A {
public:
int c;
};
class D : public B, public C {
public:
int d;
};
在上面的代码中,类 B 和类 C 继承类 A 时都使用了 virtual 关键字,这样就将类 A 标记为虚基类。此时,在类 D 中,类 A 的成员变量只出现了一次,且只有一个虚基类指针指向类 A 的成员变量。这样就解决了数据冗余和二义性的问题。
多态
一种接口多种方法
在C++中,多态是面向对象编程(OOP)的一个核心概念,它允许我们使用一个通用接口来表示不同类型的对象。多态主要有两种形式:编译时多态(静态多态)和运行时多态(动态多态)。
-
编译时多态:通过函数重载和运算符重载实现。在编译时,根据函数签名或运算符的参数类型来确定调用哪个版本的函数或运算符。
-
运行时多态:通过继承和虚函数实现。运行时多态允许我们在运行时根据对象的实际类型来调用合适的成员函数。为了实现运行时多态,我们需要以下几个步骤:
a. 创建一个基类,并声明一个或多个虚函数(使用关键字
virtual)。 b. 创建一个或多个派生类,继承自基类,并覆盖基类的虚函数。 c. 使用基类指针或引用来指向派生类的对象。 d. 通过基类指针或引用调用虚函数,将会执行对象实际类型对应的虚函数实现。
//运行时多态
#include <iostream>
class Shape {
public:
virtual void draw() const {
std::cout << "Drawing a shape." << std::endl;
}
};
class Circle : public Shape {
public:
void draw() const override {
std::cout << "Drawing a circle." << std::endl;
}
};
class Rectangle : public Shape {
public:
void draw() const override {
std::cout << "Drawing a rectangle." << std::endl;
}
};
void drawShape(const Shape& shape) {
shape.draw();
}
int main() {
Circle circle;
Rectangle rectangle;
drawShape(circle); // 输出:Drawing a circle.
drawShape(rectangle); // 输出:Drawing a rectangle.
return 0;
}
虚函数
作用:实现多态性。
虚函数的动态绑定是指:在运行时调用的函数版本,而不是在编译时确定调用的函数版本
构造函数不能为虚函数,但可以将析构函数定义为虚函数
只有成员函数才能定义为虚函数
静态函数不能定义为虚函数
声明函数时需要使用
virtual,定义函数时不需要virtual
在基类中声明的虚函数并不是使基类中的该函数变成虚函数,而是派生类中的同名函数变成虚函数
在基类中声明虚函数时,实际上是在基类和其派生类中都创建了虚函数表,并将虚函数的指针存储在虚函数表中。当派生类中的同名函数覆盖基类中的虚函数时,编译器会将派生类中的虚函数指针覆盖掉基类中的虚函数指针,从而实现动态绑定。
因此,在基类中声明虚函数是为了在派生类中覆盖该函数并实现多态性。同时,在基类中的虚函数也可以在基类中被调用,这并不会影响派生类中的同名函数的虚函数性质。
虚析构函数
Base *p = new Subclass;
delete p;//此时,析构过程只析构了基类,派生类并没有析构,导致资源回收不完整
为了解决这一问题,需要将基类的析构函数声明为 虚析构函数
#include <iostream>
#define pri() std::cout<<__func__<<" "<<__LINE__<<std::endl;
class Demo{
private :
int value;
public :
Demo(){
}
Demo(int value):value(value){
pri();
}
virtual ~Demo(){
pri();
}
};
class Son : public Demo{
private :
int val;
public :
Son(int value):val(value){
pri();
}
virtual ~Son(){
pri();
}
};
int main(){
{
Demo *d1=new Son(100);
delete d1;
}
return 0;
}
//结果
Son 22
~Son 25
~Demo 14
C++虚函数表
在类中,如果用virtual定义了一个虚函数,那么C++会在内存该类的内存中的第一个区域开辟一个执行虚函数表的指针,存放着虚函数的地址。
结构如下图所示

#include <iostream>
using namespace std;
#define pri() cout<<__func__<<" "<<__LINE__<<endl
typedef void (*func)(void);
class Base{
private:
int a;
int b;
public:
Base(int a , int b):a(a),b(b){}
virtual void getA(){pri();};
virtual void getB(){pri();};
};
int main(){
Base obj(122,22210);
cout<<"a:"<< *(int *)((short*)&obj+4)<<endl;
cout<<"a:"<< *(int *)((char*)&obj+8)<<endl;
cout<<"a:"<< *(int *)((int*)&obj+2)<<endl;
cout<<"a:"<< *(int *)((long )&obj+1)<<endl;
cout<<"b:"<< *((int *)((long*)&obj+1)+1)<<endl;
// func p = (func)(* (int*) (*((int*)&obj))+1);
// p();
cout<<sizeof(obj)<<endl;
// p = (func)* ((int*)(*(int*)&obj)+2);
// p();
}
覆盖、重载、隐藏
| 名目 | 覆盖 | 隐藏 | 重载 |
|---|---|---|---|
| 作用域 | 不同 | 不同 | 相同 |
| 函数名 | 相同 | 相同 | 相同 |
| 参数 | 相同 | 可以不同 | 不同 |
| 返回值类型 | 相同 | 可以不同 | 不同 |
| 有无virtual修饰 | 必须有virtual,不能有static修饰 | 可有可无 | 可有可无 |
抽象类
在类内部把函数声明变成抽象函数,然后在虚函数后面加上`=0`就可以声明为抽象类,不能去实现,只能在其他类继承该类
抽象类不能有实例化对象
例子:
class Base{
public:
virtual void print()=0;
protected:
int y;
private:
int x;
}
派生类必须实现其所继承的所有纯虚函数,否则该派生类也会成为抽象类。如果派生类只实现了部分纯虚函数,而没有实现全部纯虚函数,那么该派生类仍然是抽象类,并且不能被实例化。
限制构造
限制访问权限
构造函数默认权限是public ,如果设定为 protected 权限那么则为限制构造。
通过派生类和友元访问
动态联编
写在前面
动态联编是一种实现运行时多态的机制。在编程语言中,多态性是指同一份代码可以适用于不同的数据类型或对象,以实现不同的行为。而运行时多态是指在程序运行时根据实际对象的类型决定调用哪个具体的方法或函数。动态联编是实现运行时多态的一种方式,它将方法或函数的调用与具体实现的绑定延迟到程序运行时。当程序运行时,动态联编会根据实际对象的类型来确定调用哪个具体的方法或函数实现,以实现运行时多态。这个过程也被称为“动态绑定”。
相对地,静态联编则是在编译时将方法或函数的调用与具体实现的绑定确定下来,而不考虑实际对象的类型。因此,静态联编不支持运行时多态。
1.静态联编:静态绑定:早绑定
2.动态联编:动态绑定:晚绑定
C++ 动态联编(Dynamic Binding)是指在程序运行时,根据对象的实际类型来确定调用哪个成员函数的机制。它是面向对象编程的核心特性之一,允许我们在运行时多态地处理不同类型的对象。动态联编主要依赖于虚函数(virtual functions)来实现。
动态联编例子
class Base {
public:
virtual void display() {
cout << "Base display" << endl;
}
};
在这个例子中,我们定义了一个名为 Base 的基类,并声明了一个虚函数 display。
class Derived1 : public Base {
public:
void display() override {
cout << "Derived1 display" << endl;
}
};
class Derived2 : public Base {
public:
void display() override {
cout << "Derived2 display" << endl;
}
};
我们定义了两个名为 Derived1 和 Derived2 的派生类,分别覆盖了基类 Base 的虚函数 display。
int main() {
Base* base_ptr;
Derived1 d1;
Derived2 d2;
//向上隐式转换
base_ptr = &d1;
base_ptr->display(); // 调用 Derived1 的 display 函数
base_ptr = &d2;
base_ptr->display(); // 调用 Derived2 的 display 函数
return 0;
}
我们使用了一个基类指针 base_ptr 来操作派生类对象 d1 和 d2。当我们通过基类指针调用 display 函数时,会根据实际对象的类型来调用相应的派生类成员函数。这就是动态联编的作用。
异常
让一个函数在发现了自己无法处理的错误时抛出(throw)一个异常,然后它的(直接或者间接)调用者能够处理这个问题。
机制
try(检查)-->throw(抛出)-->catch(捕获)
try{
//检查错误并抛出错误
if(检错)
throw 异常1;
if(检错)
throw 异常2;
....
if(检错)
throw 异常n
}catch(异常1){
处理异常
}catch(异常2){
处理异常
}
...
catch(异常n)
{
处理异常
}
e.g.
int mydiv(int x, int y)throw (invalid_argument)//指定函数可以抛出异常 invalid_argument
{
if(0 != y)
return x/y;
#if 0
else
return -1;//有问题
#endif
#if 0
invalid_argument tmp("invalid argument");
throw tmp;
#else
throw invalid_argument("invalid argument");//抛出异常并指定异常原因
#endif
}
int main()
{
int x = 1, y=0;
try{
cout << mydiv(x,y) << endl;
}catch(invalid_argument &iserr){//捕获异常
cout << iserr.what() << endl;//打印异常原因
}
return 0;
}
自定义异常
两个例子,第一个例子是自己写的用来处理除数为0,但是有问题于是让ChatGPT改了一下(不得不说ChatGPT4确实牛,一次就改好了,不像3.5),然后自己又把自己写的改好了,两个方式不一样,GPT写的是继承了std::invalid_argument
//第一个
#include <iostream>
#include <stdexcept>
//GPT改成了不继承invalid_arrgument
class MyException{
public :
explicit MyException(const char * what_arg):errmsg(what_arg){}//explicit修饰该对象不能隐式转换
virtual ~MyException()noexcept{}
const char * what()const noexcept;
private:
const char * errmsg;
};
const char *MyException::what() const noexcept{
return errmsg;
}
int exceptionfordiv(int x,int y) noexcept(false){
if(0!=y)
return (x/y);
throw MyException("zero has been division");
}
int main()
{
int x,y;
std::cin>>x>>y;
try{
exceptionfordiv(x,y);
}catch (const MyException &excp){
std::cout<<excp.what()<<std::endl;
}
return 0;
}
第二个
#include <iostream>
#include <stdexcept>
class MyException: public std::invalid_argument {
public:
explicit MyException(const char * what_arg): std::invalid_argument(what_arg) {}
//自己就是上面这错了,因为继承了invalid_argument 但是看源码发现它没有无参构造方法,所以需要手动写它的有参构造
virtual ~MyException() noexcept {}
};
int exceptionfordiv(int x, int y) noexcept(false) {
if (0 != y)
return (x / y);
throw MyException("zero has been division");
}
int main() {
int x, y;
std::cin >> x >> y;
try {
std::cout<<exceptionfordiv(x, y)<<std::endl;
} catch (const MyException &excp) {
std::cout << excp.what() << std::endl;
}
return 0;
}
刚好这两种也是自定义的两种异常
- 从标准基类派生
- 完全自己实现
注意有些版本需要在g++编译时加 -std=c++0x
转换函数
本质
实现类类型的强转(实质转换函数就是 运算符的重载,只不过重载的不是基本的内置数据类型,而是自定义的类类型)
语法
语法形式:
operator 类型名( ) [const]
{
实现转换的语句
}
2.转换函数的基本规则:
1> 转换函数只能是成员函数,无返回类型,空参数。
2> 不能定义到void的转换,也不允许转换成数组或者函数类型。
3> 转换常定义为const形式,原因是它并不改变数据成员的值。
explicit关键字
在C++中,explicit关键字用来修饰类的构造函数,被修饰的构造函数的类,不能发生相应的隐式类型转换
给单参数的构造函数使用explicit关键字,阻止可能产生的隐式转换:由成员变量类型转换为类类型
在C++中,explicit是一个关键字,用于修饰类构造函数,表示该构造函数不能用于隐式类型转换,只能用于显式地创建对象。
举个例子,考虑一个类MyClass,它有一个接受一个整数参数的构造函数:
class MyClass {
public:
MyClass(int x) : value(x) {}
private:
int value;
};
在这种情况下,我们可以使用该构造函数隐式地创建一个MyClass对象:
MyClass obj = 42;
这里,整数值42被隐式地转换为一个MyClass对象,这种行为可能会导致不必要的类型转换或潜在的错误。如果我们希望避免这种行为,可以在构造函数前加上explicit关键字:
class MyClass {
public:
explicit MyClass(int x) : value(x) {}
private:
int value;
};
现在,我们不能再使用隐式类型转换来创建MyClass对象:
MyClass obj = 42; // 这行代码会编译错误
MyClass obj2(42); // 必须显式地调用构造函数来创建对象
通过使用explicit关键字,我们可以避免隐式类型转换导致的潜在错误,并且代码更加清晰易懂。需要注意的是,explicit只能用于单参数构造函数,对于多参数构造函数则不起作用。
C++标准转换函数
编译时转换:reinterpret_cast、const_cast、static_cast
运行时候转换:dynamic_cast
reinterpret_cast
reinterpret_cast(expression)
将一个类型的指针转换为另一个类型的指针,它也允许从一个指针转换为整数类型
const_cast
const_cast( expression)
const指针与普通指针间的相互转换,注意:不能将非常量指针变量转换为普通变量
static_cast(普通类类型转换)
static_cast(expression)
主要用于基本类型间的相互转换,和具有继承关系间的指针或者引用类型转换
#include <iostream>
class SubDemo;
class Demo{
private :
int x;
int y;
public :
Demo(){}
Demo(int x,int y):y(y),x(x){}
virtual ~Demo(){}
operator SubDemo();
int getX ()const{
return x;
}
};
class SubDemo:public Demo{
private :
int z;
int x;
public :
SubDemo(int y,int x):z(y),x(x){}
virtual ~SubDemo(){}
int getZ ()const{
return z;
}
int getX ()const{
return x;
}
};
Demo:: operator SubDemo(){
return SubDemo(this->x,this->y);
}
int main()
{
Demo demo(666,100);
SubDemo subDemo(999,65);
subDemo = static_cast<SubDemo>(demo);//向下隐式转换
demo = static_cast<Demo>(subDemo);//向上隐式转换
//subDemo = demo;
std::cout<<subDemo.getZ()<<std::endl;
std::cout<<subDemo.getX()<<std::endl;
return 0;
}
//输出结果
666
100
dynamic_cast(有虚函数的类型转换)
dynamic_cast(expression)
只有类中含有虚函数才能用dynamic_cast;仅能在继承类对象间转换
dynamic_cast具有类型检查的功能,比static_cast更安全
智能指针
需要添加memory头文件
unique_ptr
同时只允许一个指针指向同一片空间
例子
#include <iostream>
#include <memory>
class Demo{
private :
int x;
public :
Demo(int x):x(x){}
~Demo(){}
void fun(){
std::cout<<__func__<<__LINE__<<std::endl;
}
};
void test(){
std::unique_ptr <Demo> p (new Demo(10));
p->fun();
//可以,因为他们不是同一片地址
std::unique_ptr <Demo> q (new Demo(300));
q->fun();
//不行,因为同时只允许一个指针指向同一片地址
//std::unique_ptr <Demo> s=p;
}
shared_point
实现多个指针,共享同一个对象,并且能够自动管理被指向对象的内存。
注意,当链接到该对象的连接数为0时,系统才自动回收类似于Linux中的硬链接,可以跳转到Linux和C高级中去看
#include <iostream>
#include <memory>
#define pri() std::cout<<__func__<<__LINE__<<std::endl
class Demo{
private:
int x;
public :
Demo(int x):x(x){
pri();
}
virtual ~Demo(){
pri();
}
void fun (){
pri();
}
};
void test(){
std::shared_ptr <Demo> p= std::make_shared<Demo>(10);
//std::shared_ptr <Demo> s(new Demo(99));
std::shared_ptr<Demo>q=p;
//std::weak_ptr<Demo> q = p;
p->fun();
q->fun();
//q.lock()->fun();
}
//运行结果为
Demo10
fun16
fun16
~Demo13
可以看到Demo对象只被创建和析构了一次,所以说明shared_ptr只会指向一个对象。但是需要注意
std::shared_ptr <Demo> p= std::make_shared<Demo>(10);
最开始写的时候写成了
std::shared_ptr <Demo> p= std::make_shared<Demo>(Demo(10));
导致结果为创建了一次,析构了两次,然后通过看make_shared的源码发现参数写错了,源码如下
/**
* @brief Create an object that is owned by a shared_ptr.
* @param __args Arguments for the @a _Tp object's constructor.
* @return A shared_ptr that owns the newly created object.
* @throw std::bad_alloc, or an exception thrown from the
* constructor of @a _Tp.
*/
template<typename _Tp, typename... _Args>
inline shared_ptr<_Tp>
make_shared(_Args&&... __args)
{
typedef typename std::remove_cv<_Tp>::type _Tp_nc;
return std::allocate_shared<_Tp>(std::allocator<_Tp_nc>(),
std::forward<_Args>(__args)...);
}
代码看不懂,但是可以看到上面的注释,__args参数为对象的构造器,所以不能填Demo(10),只能填10
weak_ptr
在上面的代码中test函数中的注释代码使用了weak_ptr,可以看到weak_ptr类型的智能指针又一个lock()函数,可以通过lock()获取指向一个被管理对象的指针。
它用于解决 std::shared_ptr 所存在的循环引用问题。与 std::shared_ptr 不同的是,std::weak_ptr 不会增加引用计数,因此不会影响对象的析构。
weak_ptr特点
- 可以通过
lock()函数获取一个指向被管理对象的std::shared_ptr对象,但是如果被管理对象已经被销毁,lock()函数会返回一个空指针。 std::weak_ptr不能直接访问被管理对象的成员函数和成员变量,因为它不会增加引用计数。std::weak_ptr的构造函数可以接受一个std::shared_ptr对象,也可以接受另一个std::weak_ptr对象。std::weak_ptr可以使用expired()函数判断被管理对象是否已经被销毁。
注意:如果被管理的对象已经销毁,lock()会返回一个空指针,此时可以通过expired()函数判断。