admin管理员组文章数量:1539571
1.C++ 函数重载机制
C++中的函数重载是面向对象编程中的一个重要特性,它允许在同一个作用域内定义多个名称相同但参数列表不同的函数。函数重载可以基于以下几种不同的参数差异:
参数数量不同: 函数可以重载,如果它们接受不同数量的参数。
参数类型不同: 即使参数数量相同,只要参数类型不同,函数也可以重载。
const修饰符 如果函数的参数为指针或引用,可以通过const修饰符来区分重载版本。
默认参数: 虽然默认参数可以改变参数的数量,但需要注意,默认参数可能导致重载解析的歧义。
原理
在C++中,函数重载通过所谓的名字修饰(name mangling)机制来实现,编译器会根据函数的参数类型和数量生成不同的内部名称,这样在链接时就能正确地调用相应版本的函数
问题的引入:在实际开发过程中我们需要实现几个功能类似的函数,只是实现该函数的内部细节不一样,假如我们想要实现两个数据的加法操作,被操作数是可以是多个类型的变量,我们需要在函数参数列表上,写入两个相同或者不同的变量地址传入到函数内部中;
在C语言编程中,由于函数不可重名导致浪费程序的名字空间,例如
int add1(int* a,int * b){
*a = *a + *b;
return a;
}
double add2(double* a,int * b){
*a = *a + *b;
return a;
}
在C++中我们可以使用函数重载类解决该类问题,只需要参数列表的数据位置,大小,类型不同即可;
int add(const int *a,const int *b){
return *a + *b;
}
double add(const double *a,const int *b){
return *a + *b;
}
int add(const int *a, const double *b){
return *a + *b;
}
int main(int argc, char const *argv[])
{
int a = 10 , b = 20;
double c = 12.5;
std::cout << "add:" << add(&a,&b) << std::endl;
std::cout << "add:" << add(&c,&b) << std::endl;
std::cout << "add:" << add(&b,&c) << std::endl;
return 0;
}
//结果
add:30
add:32.5
add:32
值得注意的是,函数重载是基于编译时参数类型来决定的,因此在函数重载时,返回类型并不参与决定过程。这意味着不能仅通过改变返回类型来重载函数。
重载函数时还应该避免产生歧义
//错误 函数的返回值不能作为函数的重载判断
double add(const int *a, const double *b){
return *a + *b;
}
int add(const int *a, const double *b){
return *a + *b;
}
//错误
int Init(int a,double b){}
int Init(int a, double b = 10){}
//重定义的错误
heavy_debug.c:19:5: error: redefinition of ‘int Init(int, double)’
int Init(int a, double b = 10)
函数重载是C++提供的一种便捷的编程机制,它可以使代码更加清晰和易于维护,但使用时需要注意避免可能的歧义
const修饰符 如果函数的参数为指针或引用,可以通过const修饰符来区分
void fun(const int &a){
}
void fun(int a){
}
2.函数默认参数
在C++中,函数默认参数是指在函数原型中为参数指定一个默认值。如果在调用函数时没有提供相应的实参,编译器将自动使用这个默认值。这样,可以使得函数更加灵活,能够在不同的调用场景中使用
使用默认参数时,需要注意以下几点:
-
默认参数必须从右向左定义:如果一个函数有多个默认参数,那么默认值必须从右向左依次定义。这是因为在调用函数时,你可以只提供最右边的几个参数的值,而不提供左边的参数的值。
-
默认参数值可以是常量、全局变量、静态变量,但不能是局部变量:这是因为局部变量的生命周期在函数调用结束后就结束了,而默认参数的值需要在函数调用时仍然有效。
-
默认参数可能会导致函数重载的歧义:如果你有多个重载函数,其中一些函数有默认参数,那么在调用时可能会出现歧义,因为编译器可能无法确定应该调用哪个函数。
-
默认参数在函数原型和函数定义中都要声明:如果你在函数原型中声明了默认参数,那么在函数定义中也必须声明,否则默认值将无效。
注意点:
如果函数的声明与定义是分开的,必须在其声明时设置默认值,不可以再定义时设置默认值 不能将实际值传递给引用类型参数,可以将变量作为引用类型参数默认值,而且变量必须是全局变量
3.面向过程
概念:
面向过程(Procedure-Oriented)是一种以过程为中心的编程思想。它是早期的编程范式之一,主要关注的是程序的步骤和流程。面向过程的语言,如C语言,将程序视为一系列为了完成特定任务而必须执行的操作序列。
在面向过程的编程中,程序被分解成若干个基本步骤,这些步骤通常被组织成函数或子程序。这些函数是按顺序调用的,数据在它们之间传递。
面向过程编程强调的是功能分解和模块化,即将大问题分解为小问题,再将小问题分解为可管理的任务。
特点:
01 顺序性: 程序按照一定的顺序执行,从上到下,从左到右。
02 步骤化: 将复杂的问题分解为一系列步骤,每个步骤执行一个具体的操作。
03 数据结构: 重点在于操作数据,而非数据本身。数据结构通常比较简单,如数组、结构体等。
04 函数式编程: 程序由函数或过程组成,函数接受输入参数,执行一系列操作,并可能返回一个值。
05 全局变量: 在面向过程的程序中,全局变量被广泛使用,以在函数间传递数据
面向过程编程适用于那些简单、顺序、并且可以分解为一系列线性步骤的问题。但当面对复杂、不断变化的系统时,面向过程的方法可能不够灵活和可维护,这时面向对象的编程(OOP)可能更为合适。
📕先将解决问题的步骤分析出来,然后使用函数将这些步骤实现,在一步步的步骤中按顺序调用函数最终实现功能
步骤分析--->函数实现--->顺序执行;在途中为了数据的传输和快捷会使用到数组,结构体,全局变量等...
例:将图片显示到开发板液晶屏上
01 分析步骤:
01 打开液晶屏--->02 内存映射--->03打开图片--->04 读取数据--->05 格式转换--->06 显示图片
02 函数实现
int openDisplayEquipment(char * lcdPath); //打开显示屏
int openImage(char * imagePath); //打开图片
char * readImageData(int imageFd); //数据读取
char * image2LedFormat(char * imageData); //格式转换
void showImage(char *ledData); //显示图片
03 顺序调用
int fd = openDisplayEquipment(/dev/fb0); //01 打开显示屏
int* lcd_dev = mmap(0,fd,....); //02 内存映射
int imageFd = openImage(/1.bmp); //03 打开图片
char imageData[1024] = readImageData(imageFd); //04 读取图片数据
char ledData[1024] = image2LedFormat(imageData); //05 数据格式转换
showImage(ledData); //06 显示图片
4.面向对象
概念
面向对象(Object-Oriented,简称OO)是一种编程范式,它基于“对象”的概念,将数据和操作数据的方法封装在一起。面向对象编程强调的是数据和行为的结合,以及对象之间的交互。
特点:
-
01对象(Object): 对象是面向对象编程的基本单元,它将数据和相关的行为(函数或方法)封装在一起。每个对象都是类的实例。 02.类(Class): 类是对象的蓝图或模板,定义了一组具有相同属性和方法的对象。类定义了对象的结构和它能够执行的操作。 03.封装(Encapsulation): 封装是指隐藏对象的内部状态和实现细节,仅对外暴露一个可以被调用的接口。这样可以保护对象的数据不被外部直接访问,确保了数据的安全性和一致性。 04.继承(Inheritance: 继承允许新的类(子类)继承一个或多个现有类(父类)的属性和方法。这样可以创建一个新的类,它具有现有类的特性,同时还可以添加新的特性或者覆盖现有的特性。 05.多态(Polymorphism): 多态是指不同的对象可以响应相同的消息或方法调用,但它们会根据对象的类型来执行不同的操作。这意味着一个接口可以有多种实现。 06.抽象(Abstraction): 抽象是指将对象的本质特征和行为与具体的实现细节分离。通过抽象,我们可以创建代表现实世界中事物的模型,而不必关心它们的实现细节。
面向对象编程的优势在于它的模块化、可重用性和易于维护。它适合于处理复杂系统和大规模软件的开发,因为对象可以作为独立的模块进行开发、测试和重用。面向对象编程语言包括Java、C++、Python、C#等。
世界上有很多的动物和事物,每一个动物和事物都可以看作一个对象,他们都有自己的属性和行为,对象之间通过方法(行为)来进行交互
面向对象是一种以“对象”为中心的的编程思想,把要解决的问题分解为多个对象彼此交互完成。建立出来的对象不是为了完成某个步骤而设计的,是为了描述在解决整个问题过程中的属性和行为;
例如:
人喝水
将人和杯子子分别看成一个对象; 人: 杯子 属性:手,嘴巴... 属性:容量,材质 杯子--->装水 方法:拿,喝,提.. 方法:装水 人--->拿(杯子)--->嘴(喝水)
对象:类的实例
c++是一门面向对象的语言,其中结构体被强化,变为属性和方法的组合类型
struct GirlFriend{
//属性 (数据)
int WechatId;
char *name;
//行为 (方法)
void eat( ){cout << "eat" < endl;}
void addWechat( ){cout << "addWechat" < endl;}
}GirlFriend_t;
5.对象:
将我们需要研究的任何事物和事件以及规则抽象出来,他不仅仅表示具体的事物和动物也能表示抽象的规则,事务,计划等
事物和动物: 人,键盘,电脑,小狗,小猫,飞机,大炮,车
事务\事件: 旅游,办公,学习,运动
规则: 职场规则,游戏规则...
对象的描述:
数据/属性/变量---->数据成员
概念/方法/行为---->函数成员
6.类
😡将具有相同特性和行为的对象将其抽象出来就是类;因此对象的抽象就是类,类的具体化/实例化产生的就是对象,类实际上是一种数据类型,用来描述对象的行为和方法;
抽象[哲学概念]
-
抽象是从众多的事物中抽取出共同的、本质性的特征,而舍弃其非本质的特征的过程。
-
具体地说,抽象就是人们在实践的基础上,对于丰富的感性素材通过去粗取精、去伪存真、由此及彼、由表及里的加工制作,形成概念、判断、推理等思维形式,以反映事物的本质和规律的方法。
-
实际上,抽象是与具体相对应的概念,具体是事物的多种属性的总和,因而抽象亦可理解为由具体事物的多种属性中舍弃了若干属性而固定了另一些属性的思维活动
对象:大爷 属性:年龄,性格,身高 行为:吃饭,睡觉 去除项:照顾妻子,带小孩去玩 小孩 属性:年龄,性格,身高 行为:吃饭,睡觉 去除项:上幼儿园,找老师 女人 属性:年龄,性格,身高 行为:吃饭,睡觉 去除项目:参加妇联,找男伴,带小孩 类:抽出上述相同的行为与属性 人类: 属性::年龄,性格,身高 行为::吃饭,睡觉
如何设计并使用一个类
1.设计 01归类 ::找到对象源将其属性和行为进行归类 02抽象 ::将其共有属性和行为剥离出来进行组合其他异项去除掉 03封装 ::将抽离出来的共有属性和方法结合成一个独立的系统单元,尽可能屏蔽其中细节,对外形成一个边界,只保留有限接口和外部交互即可,不太关注过程;最终形成一个类; 2.使用 04实例化对象 ::定义一个封装好的类的对象,通过对象调用此类中的属性和方法 3.示例 竞技游戏 属性:血量 蓝量 物理攻击 魔法攻击 护甲值 装备 魔抗 真伤 移速 行为:普通攻击 Q技能 W技能 E技能 大招技能 防御技能 走动
类的简单设计编程语法格式
class hero{
//变量成员 属性/数据
int m_bloodVlaue; //血量
int m_blueValue; //蓝量
int m_attackValue; //物理伤害
int m_mAttackValue; //魔法伤害
int m_armorValue; //护甲值
//函数成员 行为/方法
void setAttackA(Hero &a){a.m_bloodVlaue = 150;}
void snake(){cout << "err attack " << endl;}
}
int mian(){
Hero dema; //实例化对象::德玛
Hero less; //示例化对象::小短腿
}
类的高级设计编程语法格式
-
类的成员访问权限
类的成员权限指的是对类成员(包括数据成员和成员函数)的访问控制。C++提供了三种访问权限,分别是public(公有)、private(私有)和protected(保护)。
-
public(公有):
-
公有成员可以在类的内部和外部被访问。
-
任何拥有类对象的代码都可以访问公有成员。
-
-
private(私有):
-
私有成员只能在类的内部被访问。
-
即使是类的实例也不能直接访问私有成员。
-
通常,类的数据成员被设置为私有,以确保数据的封装性,防止外部直接操作数据。
-
-
protected(保护):
-
保护成员可以在类的内部和继承类中被访问。
-
类的外部无法直接访问保护成员。
-
当类被继承时,派生类可以访问基类的保护成员,但外部代码不能。
-
-
类成员的默认属性是私有的
public 共有 一般将对外使用的函数接口设置为共有权限 private 私有 一般将对外不可访问的变量成员和函数成员设置为私有权限 protected 保护权限 一般将继承类可以访问的属性和行为设置为保护,不是继承者无法访问
class hero{
默认权限私有,类外无法访问
//变量成员 属性/数据
int m_bloodVlaue; //血量
int m_blueValue; //蓝量
int m_attackValue; //物理伤害
int m_mAttackValue; //魔法伤害
int m_armorValue; //护甲值
Public: //公有接口
//函数成员 行为/方法
void setAttackA(Hero &a){a.m_bloodVlaue = 150;}
void snake(){cout << "err attack " << endl;}
void setData(int boold,int blue = 0,int attack = 150,int mattack = 100,int armor =100){
m_bloodVlaue = boold; //血量
m_blueValue = blue; //蓝量
m_attackValue = attack; //物理伤害
m_mAttackValue = mattack; //魔法伤害
m_armorValue = armor; //护甲值
}
}
int mian(){
Hero dema; //实例化对象::德玛
dema.setData(1000);
Hero less; //示例化对象::小短腿
less.setData(1000,500,200,300,150);
}
7. 封装(Encapsulation)
😡封装是指隐藏对象的内部状态和实现细节,仅对外暴露一个可以被调用的接口。这样可以保护对象的数据不被外部直接访问,确保了数据的安全性和一致性
😡类的设计尽可能将公有接口和实现细节分开,公有接口表示设计的抽象组件。将细节放在一起并且将他们抽象分开被称为封装。
-
封装的第一例子是数据隐藏(将数据放在类私有部分中)是一种封装,将实现细节细节隐藏在私有部分中;
-
封装的第二例子是,将类函数定义和类声明放在不同文件中,方便我们维护,进行模块化,结构化
#include <iostream>
#include <cstring>
//类定义
typedef class hero
{
//变量成员 属性/数据
int m_bloodVlaue; //血量
int m_blueValue; //蓝量
int m_attackValue; //物理伤害
int m_mAttackValue; //魔法伤害
int m_armorValue; //护甲
public: //公有属性成员声明
//函数接口::类内声明,类外定义
void setAttackA(hero &a);
void snake();
void setData(int boold,int blue = 0,int attack = 150,int mattack = 100,int armor =100);
void getData();
}hero_t;
//类函数定义
void hero_t::setAttackA(hero &a){
if(a.m_armorValue > m_attackValue)
hero::snake();
else
a.m_bloodVlaue -= (m_attackValue - a.m_armorValue);
}
void hero_t::setData(int boold,int blue,int attack,int mattack,int armor){
hero_t::m_bloodVlaue = boold;
hero_t::m_blueValue = blue;
hero_t::m_attackValue = attack;
hero_t::m_mAttackValue = mattack;
hero_t::m_armorValue = armor;
}
void hero_t::snake(){
std::cout << "伤害免疫" << std::endl;
};
void hero_t:: getData(){
if(m_bloodVlaue <= 0)
{
std::cout << " 死亡" << std::endl;
}
else
{
std::cout << "blood value :" << m_bloodVlaue << std::endl;
std::cout << "attack value :" << m_attackValue << std::endl;
std::cout << "m_blue value :" << m_blueValue << std::endl;
std::cout << "mAttack value :" << m_mAttackValue << std::endl;
std::cout << "armor vlaue :" << m_armorValue << std::endl;
}
}
int main(int argc, const char** argv) {
hero_t dema;
hero_t luban;
dema.hero::setData(10,20,30,40,50);
luban.setData(1000,2000,3000,4000,5000);
std::cout << "[-----dema Init-----]" << std::endl;
dema.getData();
std::cout << "[-----luban Init-----]" << std::endl;
luban.getData();
dema.setAttackA(luban);
luban.setAttackA(dema);
std::cout << "[-----dema vlaue-----]" << std::endl;
dema.getData();
std::cout << "[-----luban vlaue-----]" << std::endl;
luban.getData();
return 0;
}
//运行结果
[-----dema Init-----]
blood value :10
attack value :30
m_blue value :20
mAttack value :40
armor vlaue :50
[-----luban Init-----]
blood value :1000
attack value :3000
m_blue value :2000
mAttack value :4000
armor vlaue :5000
伤害免疫
[-----dema vlaue-----]
死亡
[-----luban vlaue-----]
blood value :1000
attack value :3000
m_blue value :2000
mAttack value :4000
armor vlaue :5000
类的大小计算
空类不空
😡在C++中,即使一个类没有任何数据成员,它的大小也不是零字节。这是因为每个实例化对象的地址必须是唯一的,以便能够区分不同的对象。如果空类的大小为零字节,那么所有空类对象的地址都将是相同的,这将导致无法通过指针或引用来区分它们。
因此,为了确保每个空类实例都有唯一的地址,编译器通常会为空类分配一个字节的内存。这个字节不包含任何有用的数据,但它确保了每个对象的地址是唯一的。这样,即使类本身没有数据,你也可以安全地创建多个实例,并通过指针或引用来操作它们。
这种行为是C++标准的一部分,确保了语言的一致性和对象的唯一性。当你使用 sizeof
运算符来查询空类的大小时,你会得到一个字节的值,即使类中没有显式的数据成员。
变量名其实是一块内存的别名,如果空类的大小为0,那么我们无法对其进行初始化,也就没意义,所以编译器会给空类一个字节大小的空间给到空类,保证它有一个独立的地址。
类大小计算
😡在C++中,类的大小计算是一个涉及数据成员、成员函数(包括虚函数)、继承、虚继承和内存对齐等多个因素的过程。以下是一些基本规则,用于计算类的大小:
-
非静态数据成员:类的大小包括所有非静态数据成员的大小。每个非静态数据成员都会占用类对象的一部分空间。
-
内存对齐:为了保证访问效率,编译器可能会在数据成员之间插入填充字节(padding)。对齐通常遵循某种规则,比如每个成员按照其类型大小的整数倍对齐。
-
成员函数:类的成员函数不占用类对象的大小,因为它们存储在所有对象共享的代码段中。
-
虚函数:如果一个类有虚函数,那么不管有多少个虚函数,通常只会添加一个指向虚函数表(vtable)的指针,这个指针的大小通常是4字节(32位系统)或8字节(64位系统)。
-
继承:当一个类派生自另一个类时,派生类的大小通常包括基类的大小。如果派生类有新的数据成员,这些成员也会被添加到类的大小中。
-
虚继承:虚继承用于解决多继承时可能出现的菱形继承问题。虚继承的类会包含一个指向虚基类表(vbtable)的指针,以及可能的一些额外信息,这些也会计算在类的大小中。
8.构造函数
在C++中,构造函数是一种特殊的成员函数,它在创建类的新对象时自动调用。构造函数的名称与类名相同,没有返回类型,甚至不能定义为void。构造函数的主要目的是初始化对象的数据成员,确保对象在使用前处于有效的状态。
构造函数可以根据参数的不同分为几种类型: 1.默认构造函数(Default Constructor): 没有参数或者所有参数都有默认值的构造函数。如果没有为类显式定义任何构造函数,编译器会自动生成一个默认构造函数,这个默认构造函数没有参数,并且其函数体为空。 2.参数化构造函数(Parameterized Constructor): 接受一个或多个参数的构造函数,用于初始化对象的数据成员。 3.拷贝构造函数(Copy Constructor): 用一个已存在的对象初始化一个新的对象时调用。如果没有显式定义拷贝构造函数,编译器会生成一个默认的拷贝构造函数,它执行成员的逐位复制。 4.移动构造函数(Move Constructor): C++11中引入,用于从临时对象或右值引用初始化对象,通过“窃取”资源来避免不必要的拷贝,提高性能。 5.委托构造函数(Delegating Constructor): C++11中引入,允许一个构造函数调用另一个构造函数,以减少代码重复。 构造函数可以是内联的,也可以是递归的(调用自身的构造函数)。此外,构造函数也可以是explicit的,这通常用于防止隐式类型转换。
8.1默认构造函数
没有参数或者所有参数都有默认值的构造函数。如果没有为类显式定义任何构造函数,编译器会自动生成一个默认构造函数,这个默认构造函数没有参数,并且其函数体为空。
typedef class hero
{
// 默认构造函数 系统自动生成 无需我们定义,如果我们定义了其他类型构造函数,那么系统不会给我们自动生成,需要我们重写
hero(){}
}hero_t;
8.1.1参数列表初始化数据成员
在C++中,你可以通过构造函数的初始化列表来初始化数据成员,而不是在构造函数体内使用赋值语句。初始化列表是在构造函数开始执行之前执行的,因此它是初始化数据成员的首选方式,尤其是对于const或引用数据成员,这些成员必须在构造时初始化。
语法: 类名::构造函数名(函数参数变量1,函数参数变量2,...): 私有数据成员1(初始化私有变量1的始化值),私有数据成员2(初始化私有变量2的始化值),...{};
示例代码:
class ConstRefExample {
public:
ConstRefExample(int x, int& y) : m_const(x), m_ref(y) {
// 构造函数体
}
private:
const int m_const;
int& m_ref;
};
😡注意:只有构造函数才能使用参数列表初始化方式进行初始化
//类定义
typedef class hero
{
//变量成员 属性/数据
int m_bloodVlaue; //血量
int m_blueValue; //蓝量
int m_attackValue; //物理伤害
int m_mAttackValue; //魔法伤害
int m_armorValue; //护甲
public: //公有属性成员声明
hero();
//参数化构造函数声明
hero(int boold,int blue = 0,int attack = 150,int mattack = 100,int armor =100);
//函数接口::类内声明,类外定义
void setAttackA(hero &a);
void snake();
void setData(int boold,int blue = 0,int attack = 150,int mattack = 100,int armor =100);
void getData();
}hero_t;
//实现参数化构造函数,使用初始化列表的语法
hero_t::hero(int boold,int blue,int attack,int mattack,int armor):
m_bloodVlaue(boold),m_blueValue(blue),m_attackValue(attack),m_mAttackValue(mattack),m_armorValue(armor*1.5){}
//重写默认构造函数,使用初始化列表的语法
hero_t::hero():
m_bloodVlaue(100),m_blueValue(200),m_attackValue(300),m_mAttackValue(100),m_armorValue(100*1.5){}
//主函数
int main(int argc, const char** argv) {
hero_t dema;
hero_t luban(1000,2000,3000,4000,5000);
std::cout << "[-----dema Init-----]" << std::endl;
dema.getData();
std::cout << "[-----luban Init-----]" << std::endl;
luban.getData();
return 0;
}
/* 效果 */
[-----dema Init-----]
blood value :100
attack value :300
m_blue value :200
mAttack value :100
armor vlaue :150
[-----luban Init-----]
blood value :1000
attack value :3000
m_blue value :2000
mAttack value :4000
armor vlaue :7500
对于不能直接赋值的函数我们不能使用默认参数列表初始化
#include <iostream>
#include <cstring>
//类定义
typedef class hero
{
//变量成员 属性/数据
char *M_name; //不能直接初始化得成员
int m_bloodVlaue; //血量
public: //公有属性成员声明
hero();
//参数化构造函数声明
hero(char *name,int boold);
//函数接口::类内声明,类外定义
}hero_t;
hero_t::hero():m_bloodVlaue(200){
M_name = (char *)malloc(256);
M_name = strcpy(M_name,"未知");
if (M_name == NULL)
{
std::cout << "error " << std::endl;
}
std::cout << "name:" << M_name << std::endl;
std::cout << "bloodValue:" << m_bloodVlaue << std::endl;
}
hero_t::hero(char *name,int boold):m_bloodVlaue(boold){
M_name = (char *)malloc(256);
M_name = strcpy(M_name,name);
if (M_name == NULL)
{
std::cout << "error " << std::endl;
}
std::cout << "name:" << M_name << std::endl;
std::cout << "bloodValue:" << m_bloodVlaue << std::endl;
}
int main(int argc, char const *argv[])
{
hero_t dema;
hero_t luban((char *)"luban",3000);
return 0;
}
//结果
name:未知
bloodValue:200
name:luban
bloodValue:3000
使用初始化列表的好处还包括:
-
对于用户定义的类型,可以调用它们的构造函数而不是拷贝构造函数,这可能更高效。
-
确保了初始化的顺序与成员在类中声明的顺序一致,即使构造函数的参数列表中的参数顺序不同。
总之,初始化列表是C++中初始化数据成员的首选方式,它提供了更好的性能和更清晰的代码。对于const和引用成员,它是初始化的唯一方式。
8.2参数化构造函数
没有参数或者所有参数都有默认值的构造函数。如果没有为类显式定义任何构造函数,编译器会自动生成一个默认构造函数,这个默认构造函数没有参数,并且其函数体为空。
//类定义
typedef class hero
{
//变量成员 属性/数据
int m_bloodVlaue; //血量
int m_blueValue; //蓝量
int m_attackValue; //物理伤害
int m_mAttackValue; //魔法伤害
int m_armorValue; //护甲
public: //公有属性成员声明
//默认构造函数声明
hero();
//参数化构造函数声明
hero(int boold,int blue = 0,int attack = 150,int mattack = 100,int armor =100);
//函数接口::类内声明,类外定义
void setAttackA(hero &a);
void snake();
void getData();
}hero_t;
//定义参数化构造函数
hero_t::hero(int boold,int blue,int attack,int mattack,int armor){
hero_t::m_bloodVlaue = boold;
hero_t::m_blueValue = blue;
hero_t::m_attackValue = attack;
hero_t::m_mAttackValue = mattack;
hero_t::m_armorValue = armor;
}
//重写默认构造函数
hero_t::hero(){
hero_t::m_bloodVlaue = 0;
hero_t::m_blueValue = 0;
hero_t::m_attackValue = 0;
hero_t::m_mAttackValue = 0;
hero_t::m_armorValue = 0;
}
//主函数
int main(int argc, const char** argv) {
hero_t dema; //调用默认构造函数
hero_t luban(1000,2000,3000,4000,5000); //调用参数化构造函数
std::cout << "[-----dema Init-----]" << std::endl;
dema.getData();
std::cout << "[-----luban Init-----]" << std::endl;
luban.getData();
return 0;
}
//运行效果
[-----dema Init-----]
blood value :0
attack value :0
m_blue value :0
mAttack value :0
armor vlaue :0
[-----luban Init-----]
blood value :1000
attack value :3000
m_blue value :2000
mAttack value :4000
armor vlaue :5000
8.3 拷贝构造函数
用一个已存在的对象初始化一个新的对象时调用。如果没有显式定义拷贝构造函数,编译器会生成一个默认的拷贝构造函数,它执行成员的逐位复制。
class Hero{
char *name;
char *pswd;
public:
Hero(); //默认构造函数
//编译器会生成一个默认的拷贝构造函数,它执行成员的逐位复制。
Hero(const Hero& ra){
ra.name = name;
ra.pswd = pswd;
}
}
系统自己生成的拷贝构造函数只是进行值的拷贝,这种拷贝被称为浅拷贝;其被拷贝的对象的指针
Hero(const Hero& ra){
this->name = ra.name;
this->pswd = ra.pswd;
}
我们希望在使用一个对象来初始化另外一个变量时,其堆空间是独立的,而不是共用的;
系统自动生成的拷贝构造函数只是做值的拷贝,称为“浅拷贝”,但是我们想要的效果时每一个类成员指针变量都应该有自己独立的空间,而不是和别人共用;
//深拷贝示例代码
Hero(const Hero& ra){
this->name = new char[256];
strcpy(this->name,name);
this->pswd = new char[256];
strcpy(this->pswd,pswd);
}
正确的做法时,要给类的指针成员申请独立的空间,然后对其进行复制,这被称为“深拷贝”;
使用自定义拷贝构造函数的时间:
当类中有指针成员需要申请堆空间来存储被拷贝的内存时。
8.4 移动构造函数 C++11语法
C++ 中的移动构造函数是一种特殊的构造函数,它在对象被创建时使用,用于从另一个同类型的对象那里“窃取”资源,而不是复制资源。这通常发生在源对象不再需要其资源时,从而可以高效地将资源从一个对象转移到另一个对象,避免不必要的复制操作,提高程序性能。
语法:类名(类名 &&对象名)
class_name(class_name &&source){
}
//资源窃取 吸心大法
Hero_t:: Hero(Hero && src) noexcept{
this->M_name = src.M_name;
src.M_name = nullptr;
this->m_bloodVlaue = src.m_bloodVlaue;
src.setBloodVlaue(0);
}
8.4.1noexcept 关键字
它用于指定一个函数是否承诺不抛出异常。noexcept
可以用作运算符或修饰符。
1. 运算符:当 noexcept 用作运算符时,它接受一个表达式作为参数,并返回一个布尔值,指示该表达式是否承诺不抛出异常。例如:
void my_function() noexcept(noexcept(expression)) {
// ...
}
在这里,my_function是否承诺不抛出异常取决于expression是否承诺不抛出异常。
2. 修饰符:当 noexcept 用作修饰符时,它放在函数的参数列表之后,表示该函数承诺不抛出异常。例如:
void my_function() noexcept {
// ...
}
在这里,my_function被声明为不会抛出异常。
使用 noexcept
的好处是,它允许编译器进行优化。如果一个函数被声明为 noexcept
,编译器可以生成更高效的代码,因为它不需要考虑异常处理的路径。此外,当异常发生时,如果栈展开过程中遇到 noexcept
函数,编译器可以采取更快的处理方式,因为它知道这些函数不会抛出异常,从而减少了异常处理的复杂性。
如果一个 noexcept
函数确实抛出了异常,程序会调用标准库中的 std::terminate函数,这通常会导致程序立即终止。因此,只有在确信函数不会抛出异常时,才应该将其声明为 noexcept。
在 C++11 中,移动构造函数和移动赋值运算符通常被声明为 noexcept
,因为它们旨在高效地转移资源,而不应该抛出异常。如果移动操作可能失败,则不应将其声明为 noexcept
。
8.5 委托构造函数
在 C++ 中,委托构造函数是 C++11 标准引入的一个特性,它允许一个构造函数调用另一个构造函数,从而避免代码重复,并使构造函数之间的关系更加清晰。
当我们有一个类,它有多个构造函数,而这些构造函数之间有很多共同的初始化代码时,委托构造函数就非常有用。通过委托构造函数,我们可以将共同的初始化代码放在一个构造函数中,其他构造函数则委托给这个构造函数来完成初始化。
还没学习过,先不写 .........
9.析构函数
在C++中,析构函数是类的一个特殊成员函数,它在 对象的生命周期结束时自动调用,用于执行对象销毁前的清理工作。析构函数的名称与类名相同,前面加上一个波浪形(~)作为前缀,没有参数和返回类型,甚至不能定义为void。
析构函数的主要目的是释放对象在生命周期内分配的所有资源,例如动态内存、文件句柄、网络连接等。这有助于防止内存泄漏和其他资源泄漏问题。
如果一个类没有显式定义析构函数,编译器会自动生成一个默认的析构函数,这个默认析构函数的函数体为空。如果类中有需要手动释放的资源,你应该显式定义析构函数
typedef class hero{
char *M_name;
int m_bloodVlaue;
public: //公有属性成员声明
hero(); //默认构造函数声明
~hero(); //析构函数声明
hero(char *name,int boold); //参数化构造函数声明
}hero_t;
//类外实现虚构函数,回收资源
hero_t::~hero(){
free(M_name);
m_bloodVlaue = 0;
std::cout << "~hero bloodValuue = " << m_bloodVlaue << std::endl;
std::cout << "~hero M_name = " << (unsigned int*)M_name << std::endl;
}
/* ... 其他函数初始... */
int main(int argc, char const *argv[])
{
hero_t dema;
hero_t luban((char *)"luban",3000);
return 0;
}
//结果
~hero bloodValuue = 0
~hero M_name = 0x7ffff74123d0
~hero bloodValuue = 0
~hero M_name = 0x7ffff7411eb0
10.This指针
C++中的this
指针是一个特殊的指针,它指向当前对象本身。在类的成员函数中,this
指针可以被用来引用当前对象的成员变量或者其他成员函数。this
指针对于处理对象之间的相互作用和区分指向同一类的不同对象非常有用。
每个当前对象都含有一个指向自身的this
指针,它作为隐藏参数自动传递给成员函数。这意味着,即使在成员函数的参数列表中没有显式地声明this
,编译器也会处理它。
😡当我们函数的形参列表变量名和私有成员名冲突是,我们可以显示的调用this指针,在函数的形参列表变量名和石油变量不一样时可以不显式调用this,系统编译时会自动添加
typedef class hero{
char *M_name;
int m_bloodVlaue;
public:
~hero();
hero(char *name,int boold = 1000);
}hero_t;
hero_t::hero(char *M_name,int m_bloodVlaue){
//当函数形参列表变量名和类私有变量重名时使用this指针进行初始化
this->M_name = (char *)malloc(256);
//this-> new char[256];
if (this->M_name == NULL)
std::cout << "this->name malloc error" << std::endl;
//当函数形参列表变量名和类私有变量重名时使用this指针进行初始化
this->m_bloodVlaue = m_bloodVlaue;
}
This指针实现方法链
😡使用this
指针的一个常见场景是在一个成员函数中返回当前对象的引用,这在需要连续调用成员函数时很有用,也称为方法链
在C++中,使用this
指针实现方法链通常涉及到在每个成员函数的末尾返回当前对象的引用。这样,您可以连续调用多个成员函数,每个函数调用都会修改同一个对象,并返回该对象的引用以供下一个函数使用。这种方法通常称为"Fluent Interface"(流畅接口)。
typedef class Hero{
char *M_name;
int m_bloodVlaue;
public: //公有属性成员声明
Hero();
~Hero();
Hero(char *name,int boold = 1000);
Hero& setBloodVlaue(int m_bloodVlaue);
Hero& setName(char *name);
Hero& getInfo();
}Hero_t, *hero_p_t;
//方法链路1 设置名字
Hero& Hero_t::setName(char *M_name){
this->M_name = new char[256];
strcpy(this->M_name,M_name);
return *this;
}
//方法链路2 设置血量
Hero& Hero::setBloodVlaue(int m_bloodVlaue){
this->m_bloodVlaue = m_bloodVlaue;
return *this;
}
//方法链路3 打印信息
Hero& Hero::getInfo(){
std::cout << "---------------------" << std::endl;
std::cout << "name:" << this->M_name << std::endl;
std::cout << "blood:" << this->m_bloodVlaue << std::endl;
return *this;
}
//析构函数 释放空间
Hero_t::~Hero(){
std::cout << "~Hero " << std::endl;
delete [] M_name;
}
//主函数
int main(int argc, char const *argv[]){
Hero_t dema;
hero_p_t luban = new Hero_t;
//方法链的使用
dema.setName( (char *)"德玛西亚").setBloodVlaue(3000).getInfo();
luban->setName( (char *)"鲁班").setBloodVlaue(2000).getInfo();
//释放资源
delete luban;
return 0;
}
//结果
---------------------
name:德玛西亚
blood:3000
---------------------
name:鲁班
blood:2000
~Hero
~Hero
为了保持链式调用的连贯性,每个成员函数都必须返回当前对象的引用。如果函数返回的不是引用,那么链式调用就会中断。此外,如果类有可能被复制或移动,那么返回引用时要确保返回的是对象的正确引用,以避免潜在的问题。
c++ 堆空间管理 New/delete
在C++中,堆空间是动态分配内存的一种方式,它与栈空间相对。栈空间主要用于存储局部变量和函数调用的上下文信息,其生命周期由编译器自动管理。而堆空间则用于存储程序运行时动态分配的内存,其生命周期由程序员负责管理。
😡C++中堆空间的管理主要通过以下几个关键字和标准库进行:
1. new 和 delete: - new 关键字用于在堆上动态分配内存,并可以调用构造函数初始化对象。 - delete 关键字用于释放由 new 分配的内存,并可以调用对象的析构函数。 2. new[] 和 delete[]: - new[] 用于动态分配数组的内存,并可以调用构造函数初始化数组中的每个元素。 - delete[] 用于释放由 new[] 分配的数组内存,并可以调用数组中每个元素的析构函数。
😡正确管理堆空间是非常重要的,因为不当的内存分配和释放可能导致内存泄漏、野指针和程序崩溃等问题。以下是一些堆空间管理的最佳实践:
01 对于使用 new 分配的每一个对象,都应该使用 delete 来释放内存。 02 对于使用 new[] 分配的每一个数组,都应该使用 delete[]来释放内存。 03 避免内存泄漏,即确保所有动态分配的内存最终都被正确释放。 04 避免野指针,即不要使用已经释放的内存。 05 使用智能指针来自动管理动态分配的内存,以减少内存泄漏和野指针的风险。
new/delete对于C语言的malloc和free最大的区别是,使用new/delete申请和释放类对象时会自动调用析构和构造函数
c++ 分文件编写 模块化编程
在C++中,源文件通常包含.cpp
扩展名,而头文件包含.h
或.hpp
扩展名。源文件用于实现类的成员函数、定义全局函数和变量,以及包含程序的main函数。头文件则用于声明类、函数和变量,以便在其他源文件中共享接口和类型定义。
头文件(.h或.hpp)
头文件通常包含以下内容:
- 类声明 - 函数原型 - 常量定义 - 宏定义 - 模板声明 - 包含其他头文件的指令
源文件(.cpp)
源文件通常包含以下内容:
- 类成员函数的定义 - 非成员函数的定义 - 全局变量和常量的定义 - 模板的具体化
设计原则
1. 接口与实现分离:头文件应该只包含接口声明,而将实现细节放在源文件中。这样可以减少编译依赖,提高编译效率。 2. 最小化头文件包含:避免在头文件中包含不必要的其他头文件,以减少编译时间。可以使用前向声明(forward declarations)来减少不必要的包含。 3. 使用头文件保护:使用#ifndef、#define和#endif指令来防止头文件被多次包含,这可以防止宏重复定义的问题。 4. 组织结构清晰:保持头文件和源文件的命名一致,并按照功能或模块组织文件结构,以便于理解和维护。 5. 避免全局变量:尽量减少全局变量和函数的使用,以避免命名空间污染和潜在的线程安全问题。 6. 使用命名空间:为了防止命名冲突,应该将代码放在命名空间中。 7. 文档注释:在头文件中使用文档注释来描述类、函数和宏的目的和使用方法,以便于其他开发者理解和使用。
例:
debug.cpp
#include "debug.hpp"
Hero_t::Hero(){
this->M_name = new char[100];
}
Hero& Hero_t::setName(char *name){
this->M_name = new char[100];
strcpy(this->M_name,name);
if (this->M_name == NULL){
std::cout << "setname erro" << std::endl;
}
return *this;
}
Hero& Hero::setBloodVlaue(int m_bloodVlaue){
this->m_bloodVlaue = m_bloodVlaue;
return *this;
}
Hero_t::~Hero(){
std::cout << "~Hero " << std::endl;
delete [] M_name;
}
Hero& Hero::getInfo(){
std::cout << "---------------------" << std::endl;
std::cout << "name:" << this->M_name << std::endl;
std::cout << "blood:" << this->m_bloodVlaue << std::endl;
return *this;
}
debug.hpp
#ifndef __DEBUG_THIS_H_
#define __DEBUG_THIS_H_
#include <iostream>
#include <cstring>
//类定义
typedef class Hero
{
//变量成员 属性/数据
char *M_name;
int m_bloodVlaue; //血量
public: //公有属性成员声明
Hero();
~Hero();
//参数化构造函数声明
Hero(char *name,int boold = 1000);
Hero& setBloodVlaue(int m_bloodVlaue);
Hero& setName(char *name);
Hero& getInfo();
//函数接口::类内声明,类外定义
}Hero_t, *hero_p_t;
#endif // !__DEBUG_THIS_H_
mian.cpp
#include "class_debug.hpp"
int main(int argc, char const *argv[])
{
Hero_t dema;
Hero_t luBan;
dema.setName((char*)"德玛").setBloodVlaue(3000).getInfo();
luBan.setName((char*)"鲁班").setBloodVlaue(3000).getInfo();
return 0;
}
类的组合
在C++中,类的组合是指一个类包含另一个类的对象作为其成员。这种包含关系可以是聚合的,也可以是继承的。在这里,我们主要讨论聚合的组合,即一个类将另一个类的对象作为自己的成员变量。
组合是一种“has-a”关系,表示一个类“有”另一个类的对象。这种关系通常用于建模现实世界中的复杂对象,它们由更简单的对象组成。
内嵌对象
将一个类对象作为另一个类的内嵌对象成员
#ifndef __DEBUG_HA_S_H_
#define __DEBUG_HA_S_H_
#include <typeinfo>
#include <iostream>
#include <cstring>
class Printer
{
private:
void* data;
public:
Printer();
~Printer();
};
class computer
{
private:
Printe Pri;
public:
computer();
~computer();
};
#endif
#include "ha-s_debug.hpp"
Printer::Printer(){
std::cout << "打印机的构造函数 " << std::endl;
}
Printer::~Printer(){
std::cout << "打印机的析构函数 " << std::endl;
}
computer::computer(){
std::cout << "计算机的默认构造函数 " << std::endl;
}
computer::~computer(){
std::cout << "计算机的析构函数函数 " << std::endl;
}
#include "ha-s_debug.hpp"
int main(int argc, char const *argv[])
{
computer asus;
return 0;
}
s@Shuai:/mnt/e/MyCode/ha-s_debug$ ./debug
打印机的构造函数
计算机的默认构造函数
计算机的析构函数函数
打印机的析构函数
s@Shuai:/mnt/e/MyCode/ha-s_debug$
当前类构造--->内嵌类对象的构造--->当前类的析构--->内嵌类对象成员的析构 当我们需要使用到某物的时候,这个东西应该早就存在了,不应该是我需要用它的时候才去生产它,这个东西应该早就有了 内嵌类对象应该比我调用类早出现
内嵌对象的初始化需要在当前类的构造函数上指明,并且使用参数列表方式进行初始化,因为参数列表初始化的成员产生时间早于创建类之前
//打印机类 定义
class Printer{
private:
void* data;
public:
Printer(int a); //打印机类参数化拷贝构造函数
~Printer();
};
class computer{
private:
Printer Pri; //内嵌类对象变量成员
public:
computer(int a) //打印机类参数化拷贝构造函数
~computer();
};
//-----------------------------函数实现------------------
Printer::Printer(int a){
this->data = new char[256];
this->data = &a;
std::cout << "打印机的析参数化构造函数 " << std::endl;
std::cout << "this->data:" << *(int *)data << std::endl;
}
//在调用当前类的构造函数之前/同时初始化类成员变量的值,调用内嵌内对象的构造函数
computer::computer(int a):Pri(a){
std::cout << "计算机类的参数化构造函数" << std::endl;
}
Printer::~Printer(){
std::cout << "打印机的析构函数 " << std::endl;
}
computer::~computer(){
std::cout << "计算机的析构函数函数 " << std::endl;
}
int main(int argc, char const *argv[])
{
computer asus(1000);
return 0;
}
上述我们主要关注的代码是
Printer::Printer(int a){
this->data = new char[256];
this->data = &a;
std::cout << "打印机的析参数化构造函数 " << std::endl;
std::cout << "this->data:" << *(int *)data << std::endl;
}
//在调用当前类的构造函数之前初始化类成员变量的值,调用内嵌内对象的构造函数
computer::computer(int a):Pri(a){
std::cout << "计算机类的参数化构造函数" << std::endl;
}
整个程序的类声明周期是:达到了我们预期的目的
computer asus(1000);
//—————————————————运行结果---------------
打印机的析参数化构造函数
this->data:1000
计算机类的参数化构造函数
计算机的析构函数函数
打印机的析构函数
内嵌类对象初始化--->调用类初始化---->内嵌类死亡---->调用类死亡
//不能再内嵌对象成员定义时使用参数化构造函数,否则会失败
class computer
{
private:
Printer Pri;
public:
computer(int a);
computer();
};
int main(int argc, char const *argv[])
{
computer asus;
asus.Pri(2000); //报错,因为该函数以及调用了默认构造函数
return 0;
}
mian.cpp:6:13: error: no match for call to ‘(Printer) (int)’
6 | asus.Pri(2000);
析构函数调用顺序
本类--->内嵌类
使用内嵌函数成员完成功能交互
判断点是否在圆上
欧几里得公式 r^2 = (x1 - x2)^2 + (y1 - y2)^2 ;
头文件
#ifndef __DEBUG_CIRCLE_H_
#define __DEBUG_CIRCLE_H_
#include <typeinfo>
#include <iostream>
#include <cstring>
//点类
class Point
{
public:
Point(int x = 0,int y = 0);
void setX(int x = 0);
void setY(int y = 0);
int getX();
int getY();
private:
int m_x;
int m_y;
};
//圆类
class Circle{
public:
Circle(int x,int y,int r);
void setR(int r);
int getR();
Point getCenter();
void setCenter(Point center);
private:
int m_r; //半径
Point m_center; //中心点;
};
void isInCirCle(Circle &C,Point &p);
#endif
cpp源码文件
#include "Circle_Debug.hpp"
//——————----------Point的函数成员实现------------------
Point::Point(int x,int y):m_x(),m_y(){}
void Point::setX(int x){this->m_x = x;}
void Point::setY(int y){this->m_y = y;}
int Point::getX(){return this->m_x;}
int Point::getY(){return this->m_y;}
//——————----------CirCle的函数成员实现------------------
void Circle::setR(int r){this->m_r = r;}
int Circle::getR(){return m_r;}
void Circle::setCenter(Point center){this->m_center = center;}
Point Circle::getCenter(){return this->m_center;}
Circle::Circle(int x,int y, int r):m_center(x,y),m_r(r){ }
//-----------------判断点是否在圆上---------------------
void isInCirCle(Circle &C,Point &p){
//两点之间距离的平方
int point2ClircleSquare = ( p.getX() - C.getCenter().getX() ) * (p.getX() - C.getCenter().getX() ) + ( p.getY()C.getCenter().getY() ) * (p.getY() - C.getCenter().getY() );
//半径的平方
int rSquare = (C.getR() * C.getR());
if (point2ClircleSquare <= rSquare )
std::cout << "点在圆上" << std::endl;
else
std::cout << "点不在圆上" << std::endl;
}
主函数文件
#include "Circle_Debug.hpp"
int main(int argc, char const *argv[]){
Circle A(0,0,20);
Point B(5,5);
isInCirCle(A,B);
return 0;
}
实现功能
s@Shuai:/mnt/e/MyCode/Circle_Debug$ g++ *.cpp *.hpp -o debug s@Shuai:/mnt/e/MyCode/Circle_Debug$ ./debug 点在圆上
本文标签: 对象
版权声明:本文标题:C++ 类与对象 内容由热心网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:https://m.elefans.com/dianzi/1727007492a1093928.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论