当前位置: 首页 > news >正文

C++之路:类基础、构造析构、拷贝构造函数

目录

  • 前言
  • 从结构体到类
  • 类的声明与使用
    • 基础声明
    • 继承声明
    • 数据与函数声明与调用
    • 声明
      • 调用
  • 类的访问修饰符
  • 类对象的内存分布
  • 类内数据相关
    • 静态变量
    • 非静态变量
  • 类成员函数相关
    • 普通成员函数
    • 友元函数
    • 构造与析构函数
      • 构造函数
      • 析构函数
    • 拷贝构造函数
      • 总结

前言

 面向对象编程有三大特性,分别是封装继承多态。这些特性在类的语法使用中都得到了充分的体现,我也预计写几篇文章来介绍一下C++的类语法。这是第一篇:类基础。
 类基础主要目的是介绍一下类的基础使用,重点在于数据函数的封装。

从结构体到类

 对于新手来说 这个概念可能比较陌生,但是提到结构体(struct)对于有C语言基础的人应该比较熟悉。在C++中结构体和类的底层实现几乎完全一致,结构体不仅可以封装函数,甚至还可以继承类。

#include <iostream>
#include <cstring>
using namespace std;    class Base_Class{
protected:int id; //占用大小为4字节的内存空间
public:Base_Class(int i=0){id = i;cout << "Base_Class constructor called." << endl;cout << "id = " << id << endl;}
};struct Derived_struct : public Base_Class
{Derived_struct(int i) : Base_Class(i) {cout << "Derived_Struct constructor called." << endl;cout << "id = " << id << endl;}
};int main() {Derived_struct obj(10);return 0;
}

输出结果为:

Base_Class constructor called.
id = 10
Derived_Struct constructor called.
id = 10

 是不是对类的感觉亲切了许多?当然在C语言中结构体里是不能封装函数的,也没有继承这个说法。数据和函数不封装到一起,那么当我处理数据的时候(比如结构体)就要去其它地方找函数(公共声明函数的地方),而不是通过对象直接可以调用处理的函数。
 打个比方,就像用勺子吃西瓜:C语言买了西瓜后要到厨房去找勺子才能吃西瓜,而C++封装的特性就是让买西瓜时,西瓜和勺子绑定在一起出售。因而C语言是面向过程编程的,C++是面向对象编程
 当然了,结构体和类底层实现相同,这不意味着在C++中结构体和类能混用。从规范上来说,结构体用于数据聚合,基本上不封装方法,就像C那样(例如坐标点封装,颜色封装)而类则用于‌对象进行封装‌,包括数据以及相关的方法

类的声明与使用

基础声明

类的基础语法声明如下:
来源菜鸟教程
举例说明:

class Box
{public:double length;   // 盒子的长度double width;  // 盒子的宽度double height;   // 盒子的高度int get_volume(){ //获得盒子的体积return length*width*height;}
};

 类里面就两样东西:数据+方法

继承声明

继承的语法如下:

class {derived_class} : {access_specifier} {base_class}

 其中,derived_class是派生类的名称,base_class是基类名称,二者之间通过: + 访问修饰符连接。访问修饰符 access-specifier 是 public、protected 或 private 其中的一个,如果未使用访问修饰符 access-specifier,则默认为 private继承。

// 基类
class Animal {
xxxx
};
//派生类
class Dog : public Animal {
xxxx
};

数据与函数声明与调用

声明

 类内变量与成员函数的声明语法类外一致,值得注意的是成员函数的具体实现可以放在类外。具体方法如下:

  1. 首先在类内声明这个函数:
class Dog {
public:void bark(); //类内声明
};
  1. 然后在类外以范围解析运算符(::) + 函数名的形式进行定义:
//类外实现
void Dog::bark(){cout << "汪汪汪" <<endl;
}

调用

 类的变量和函数的调用方法与结构体类似:

class Dog{
public: int age=10;//数据void bark() //方法{cout << "汪汪汪" <<endl;}
};
  1. 如果是类对象实例,使用.运算符访问成员变量和函数方法:
Dog wangcai;
wangcai.age; //访问变量
wangcai.bark(); //访问函数
  1. 如果是类指针,使用->运算符访问成员变量和函数方法:
Dog* wangcai;
wangcai->age; //访问变量
wangcai->bark(); //访问方法

类的访问修饰符

 访问修饰符用来控制外界对类内变量以及成员函数(类成员)的访问权限。关键字 public、private、protected 称为访问修饰符。

  1. public(公有)
     该成员变量/方法可以被外部的函数直接访问。这是权限最低的一种,类内、派生类以及类外都能访问往往用于对外接口上
  2. protected(保护)
     保护的类成员只能被类内以及派生类访问,类外无法访问。
  3. private(私有)
     如果没有声明访问权限,私有是默认的访问修饰符。是保护程度最高的一种,只能类内函数访问,派生类和类外都是不能访问的。往往只用于给内部成员函数数据交换

上面的内容总结来说就是:类内成员函数访问类内的变量/方法时没有任何限制派生类能访问public和protected类外的普通函数就只能访问public下的变量/方法(友元函数除外)

#include <iostream>
using namespace std;class Example {
public:    // 公有成员int publicVar;void publicMethod() {cout << "Public method" << endl;}private:   // 私有成员int privateVar;void privateMethod() {cout << "Private method" << endl;}protected: // 保护成员int protectedVar;void protectedMethod() {cout << "Protected method" << endl;}
};

类对象的内存分布

 首先要明确一下,类本身是一个抽象的概念,是不占用物理内存的。例如:

class Dog{
public: int age=10;//数据void bark() //方法{cout << "汪汪汪" <<endl;}
};

 如果不实例化这个类(Dog wangcai)是不会有内存占用的。提这个点是为了解答后面学习多态时,有的教材会说虚函数表存储在类里面,这样容易引起误解,类就像int 、float那样,如果不实例化是不会创造出内存空间的。

类对象实例的内存分布如下:

  1. 最开头: 虚函数表指针。
  2. 非静态成员变量。(按照声明顺序依次排布)

此外,

  • 对于静态成员变量:存储在全局数据区,不占用类实例内存空间。
  • 对于成员函数:代码存储在代码段,所有对象共享同一份函数代码。
  • 对于虚函数:虚函数实现也和成员函数一样存储在代码段。
  • 对于虚函数表:虚函数表在可执行文件中位于.rodata段(Linux/Unix)或.rdata段(Windows),程序加载后存于内存的‌常量区‌,具有只读属性。

类内数据相关

静态变量

 静态变量是类内以static为修饰符定义的变量。要点如下:

  1. 对于类中的静态变量来说,所有对象共享同一个静态变量,修改会影响所有实例。(即存储位置在全局数据区)
  2. 静态变量的生命周期‌在程序启动时初始化,程序结束时销毁。
  3. 静态变量在类内声明在类外定义
  4. 推荐使用类名访问(推荐):ClassName::staticVar

示例代码:

class Person {
public:static int count;  // 静态变量声明
};
int Person::count = 0;  // 必须在类外定义Person p1;
Person::count++;  // 推荐:通过类名访问
p1.count++;       // 不推荐:通过对象访问(合法但不清晰)

非静态变量

 又被称为实例变量,是指类内定义的普通成员变量。有以下特性:

  1. 每个对象独立存储‌,不同对象的实例变量互不影响。(即存储在实例的内存区域)
  2. 生命周期‌:随对象创建而分配,随对象销毁而释放。
  3. 必须通过对象实例进行访问。

举例:

class Person {
public:std::string name;  // 实例变量int age;           // 实例变量
};
Person p1;
p1.name = "Alice";  // 访问实例变量

类成员函数相关

普通成员函数

 要点:

  1. 普通成员函数暗含this指针,可以无需声明使用成员变量
  2. 成员函数的调用要通过对象实例进行,不能单独拎出来使用。

友元函数

 友元函数本质就是类外声明定义的函数,不与类对象绑定,即在类内用friend声明的外部函数,能突破封装性直接访问类的私有和保护成员,使其数据访问权限等同于成员函数(也就是说可以访问类的私有变量与函数)。
要点:

  1. 必须在类内使用friend修饰符进行声明。
  2. 具体的实现放在类外。

示例:

class MyClass {
private:int secret;
public:friend void peek(MyClass& obj); // 声明友元函数
};
void peek(MyClass& obj) { obj.secret = 42; } // 实现可访问私有成员

构造与析构函数

#include <iostream>
using namespace std;class MyClass {
public:int value;// 构造函数MyClass(int v) {value = v;cout << "构造函数被调用,value=" << value << endl;}// 析构函数~MyClass() {cout << "析构函数被调用,value=" << value << endl;}
};int main() {MyClass obj1(10);  // 构造函数调用{MyClass obj2(20);  // 构造函数调用}  // obj2离开作用域,析构函数调用return 0;
}  // obj1离开作用域,析构函数调用

构造函数

 构造函数是一种特殊的成员函数,有以下要点:

  1. 无任何返回值(连void也没有),可以传入参数
  2. 函数名与类名相同。
  3. 在类对象创建时执行一次,主要用来初始化类对象
  4. 不被继承,即子类的构造函数中必须要先手动调用父类的构造函数(无参构造时编译器会自动调用)。

 构造函数还有一个初始化列表的语法,可以用来初始化字段。例如上面的构造函数可以改为:

MyClass(int v):value(v){cout << "构造函数被调用,value=" << value << endl;
}

如果MyClass继承自BaseClass,初始化列表的语法也可以用来初始化构造函数,可以写成这样下面这样,以符合第四点的要求:

MyClass(int v):BaseClass(Base_args),value(v){cout << "构造函数被调用,value=" << value << endl;
};

(初始化列表时,用逗号,隔开各个参数)

析构函数

 析构函数与构造函数相呼应,就是类对象在销毁时自动调用的函数。(例如,程序结束时、局部对象离开作用域时)。要点如下:

  1. 无任何返回值(void也没有)以及不传入任何参数!
  2. 函数名为~+类名
  3. 对象销毁时自动执行,用来释放类对象手动创建的内存空间,避免内存泄漏
  4. 同样不被继承,但是自动调用(因为是无参的,编译器会自动调用)。顺序为先调用父类的析构,再调用子类的析构

拷贝构造函数

 为什么需要额外的拷贝构造函数?其实想想就能清楚,一个类对象的创建只能有两种途径:

  1. 自定义。使用默认的构造函数进行初始化。
  2. 从其它对象中复制。这时就要调用拷贝构造函数而不是构造函数进行初始化了

why?拷贝构造函数必须要写吗?为什么之前写的类没有拷贝构造函数也能编译通过?
回答这个问题首先要了解一下浅拷贝和深拷贝。
 其实C++中数据无非被分为两种:普通变量和指针变量,对于浅拷贝来说,执行的就是简单的赋值操作,不区分指针和值,这样也就会造成一种情况:对象A里有个指针p,p指向某个地址。如果此时简单的使用Class B = A这样初始化对象B就是浅拷贝。那么对象B内的指针p的值和A当中指针p的值是一样的(浅拷贝只是简单赋值),这样就会造成对象A和B的指针p指向同一块内存空间,如果B中的p改动了指向的变量(*p),那么A中p指向的值也会随之改变,在某些情况下这是很危险的。(即浅拷贝会造成多个指针指向同一个内存地址的情况)。
 对于深拷贝来说,就会区分普通变量和指针变量。对于普通变量,如int,float等就直接把值复制过去就行了。对于指针变量则会为指针成员分配新内存并复制内容,但是这一过程必须要手动实现,这就依赖我们的拷贝构造函数了!
 下面从使用场合、语法规则两方面总结拷贝构造函数:

  1. 使用场合:需要以一个已经实例化的类对象为基础,创建一个新的类对象(所谓构造),并且类的成员变量中有指针类型时,需要自定义拷贝构造函数为指针成员分配新内存,并将值赋值到新内存空间中(所谓深拷贝)。
     若未显式定义拷贝构造函数,编译器会生成默认拷贝构造函数执行‌浅拷贝‌(逐成员复制值)。拷贝构造二者缺一不可,如果仅仅是普通的赋值,例如B = A,那么默认触发浅拷贝,此时如果需要深拷贝应该要在类内重载运算符=

  2. 语法规则:
    定义:

Class_name(const Class_name& obj)

 简单来说就是构造函数的基础上,固定传入的参数必须为同类对象的‌常量引用‌(ClassName(const ClassName& obj)),若使用值传递(ClassName(const ClassName obj))会导致无限递归调用。

使用:

MyClass obj2 = obj1; // 隐式调用,此时=表示初始化而非赋值
MyClass obj3(obj1); // 显式调用

示例:

class DeepString {
public:char* data;DeepString(const DeepString& other) {data = new char[strlen(other.data) + 1]; //重新分配地址strcpy(data, other.data);  // 复制内容而非指针}
};

总结

 拷贝和构造缺一不可:

  1. 少了拷贝就会导致多个指针指向同一个地址。
  2. 少了构造就是普通的赋值操作,是在已存在对象间进行赋值(a = b)不属于根据一个已有对对象初始化一个新对象。
http://www.dtcms.com/a/265488.html

相关文章:

  • Rust Web 全栈开发(一):构建 TCP Server
  • Go基础(Gin)
  • Webpack 5 核心机制详解与打包性能优化实践
  • 牛客:HJ16 购物单【01背包】【华为机考】
  • 前端单元测试覆盖率工具有哪些,分别有什么优缺点
  • 在 Sepolia 上使用 Zama fhEVM 构建隐私代币与流动性池:全流程实战指南
  • Android音视频探索之旅 | CMake基础语法 创建支持Ffmpeg的Android项目
  • 【免费.NET方案】CSV到PDF与DataTable的快速转换
  • 音频动态压缩算法曲线实现
  • C++【成员变量、成员函数、this指针】
  • OSPF高级特性之FRR
  • Vue 项目在哪里加载「字典数据」最好
  • 基于 alpine 构建 .net 的基础镜像
  • 开源模型应用落地-让AI更懂你的每一次交互-Mem0集成Qdrant、Neo4j与Streamlit的创新实践(四)
  • Zookeeper 客户端 .net访问框架 ZookeeperNetEx项目开发编译
  • 开源 C# .net mvc 开发(六)特殊控制控制台、周期、邮件编程
  • 深度实战:Ubuntu服务器宕机排查全记录
  • 月付物理服务器租用平台-青蛙云
  • 基于 govaluate 的监控系统中,如何设计灵活可扩展的自定义表达式函数体系
  • npm ERR! code ERESOLVE npm ERR! ERESOLVE unable to resolve dependency tree
  • Python Set() 完全指南:从入门到精通
  • R语言开发记录,一
  • 前端-HTML-day1
  • Rust Web 全栈开发(二):构建 HTTP Server
  • 主流分布式中间件及其选型
  • locate命令的原理是啥
  • OpenCV CUDA模块设备层-----在GPU 上高效地执行两个 uint 类型值的最大值比较函数vmax2()
  • Frida:配置自动补全 in VSCode
  • 搭建VirtualBox-6+vagrant_2+docker+mysql5.7的步骤
  • 客户案例 | 某新能源车企依托Atlassian工具链+龙智定制开发服务,打造符合ASPICE标准的研发管理体系