细谈C++中的“类和对象”

2024-03-14 21:38
文章标签 c++ 对象 细谈

本文主要是介绍细谈C++中的“类和对象”,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

类和对象

通过C语言的学习,我们了解到C语言是面向过程的,关注的是过程,分析求解问题的步骤,通过函数调用逐步解决问题。而现在所学的C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间交互完成。类是对象的抽象或模型,而对象时类的实例下面我们将一一看到C++中类和对象的相关运用。

  • 类的定义
class className
{//主体:由成员函数和成员变量组成
};//后面有分号,和struct和union类似

class是定义类的关键字,className是类的名字,类中的元素称为类的成员,类中的数据称为类的属性或成员变量,类中的函数称为类的方法或成员函数

类的两种定义方式

  • 声明和定义全部放在类体中,须注意的是:成员函数放在类中定义,编译器可能会将其当成内联函数处理。

  • 声明放在.h文件中,定义放在.cpp中(一般采用这种方式),在类外定义需要是( 类名::函数名)指明成员属于哪个类域。

  • 类的访问限定符及封装

  • 类的访问限定符有:public(共有),private(私有),protected(保护)。
    说明:
    1.public修饰的成员在类外可以直接被访问,而private和protected修饰的成员在类外不能直接被访问。
    2.访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止。
    3.class的默认访问权限为private,struct的默认访问权限为public(因为struct要兼容C)。
    【面试题】
    问题:C++中struct和class的区别是什么?
    解答:C++需要兼容C语言,所以C++中的struct可以当成结构体去使用。另外C++中的struct可以用来定义类,和class定义类一样。区别是struct的成员默认方式是public,class的成员默认访问方式是private

  • 封装
    【面试题】
    问题:面向对象的三大特性:封装、继承、多态。在类和对象阶段,我们只研究类的封装特性,那什么是封装呢?
    解答:将数据操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
    封装本质上是一种管理,我们使用protected或private把成员封装起来,开放一些共有的成员函数对成员合理的访问。

  • 类的实例化
    用类类型创建对象的过程,称为类的实例化。类只是一个模型,定义出一个类并没有分配实际的内存空间来存储它,但一个类实例化出多个对象时,实例化出的对象占用实际的物理空间,存储类成员变量。

  • 计算类对象的大小

//类中既有成员变量,又有成员函数
class A1
{
public:void f1(){}
private:int _a;//类中仅有成员函数
};
class A2
{
public:void f2(){}
};
//类中什么也没有(空类)
class A3
{};//通过编译运行测得sizeof(A1)为4,sizeof(A2)为1,sizeof(A3)为1

结论:一个类的大小实际就是该类中成员变量之和,当然也要进行内存对齐。但要注意空类的大小,编译器给空类一个字节来唯一表示这个类。
注意:空类表示这个类实际存在,所以给空类一个字节来唯一标识。
补充一下结构体的内存对齐规则:
1.第一个成员在与结构体偏移量为0的地址处。
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
注意:对齐数 = 编译器默认的一个对齐数与该成员大小的较小值。
在VS编译器下的默认对齐数为8,gcc中的默认对齐数为4。
3.结构体的大小为:最大对齐数(所有变量类型最大者与默认对齐数取最小的)的整数倍。
4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
如果要修改默认对齐数就要用#pragma这个预处理指令

#include<iostream>
using namespace std;
//#pragma pack(自己定义的默认对齐数)要取消时括号中不填写
struct s1
{char a;int i;char c;
};
int main()
{cout << sizeof(s1) << endl;return 0;
}

注意:使用内存对齐虽然浪费空间,但提高了存储效率。

  • this指针

先定义一个时期类Date

#include<iostream>
using namespace std;
class Date
{
public:void Show(){cout << _year << "-"<< _month << "-"<< _day << endl;}Date(int year,int month,int day){_year = year;_month = month;_day = day; }
private:int _year;int _month;int _day;
};
int main()
{Date d1(2019,1,17);d1.Show();Date d2(2018,5,24);d2.Show();return 0;
}

C++编译器给每个“成员函数”增加了一个隐藏的this指针,**让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针取访问,只不过所有的操作对用户时透明的,即用户不需要来传递,编译器自动完成。

  • this指针的特性
    1.this指针的类型:类类型* const;
    2.只能在“成员函数“的内部使用;
    3.this指针本质上是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针;
    4.this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。
    所以this指针存在栈帧或寄存器,看具体的平台。
    this指针不能为空,空指针不能解引用来访问成员
#include<iostream>
using namespace std;
class A
{public:void PrintA(){cout << _a <<endl;}void Show(){cout << "Show()" << endl;}private:int _a;
};
int main()
{  A* p = NULL;p->PrintA();//能编译通过,不能运行通过p->Show();//编译通过,运行通过
}

类的6个默认成员函数

1.构造函数

构造函数是一个特殊的成员函数名字和类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有一个合适的初始值,并且在对象的生命周期内只调用一次。
需要注意的是,构造函数的名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
构造函数的特征:

  • 函数名与类名相同。
  • 无返回值。
  • 对象实例化时编译器自动调用对应的构造函数。
  • 构造函数可以重载。
    构造函数分为无参构造和带参构造函数。
class Date
{
public://1.无参构造函数Date(){}//2.带参构造函数Date(int year,in month,int day){_year=year;_month=month;_day=day;}
private:int _year;int _month;int _day;
};
void Test()
{Date d1;//调用无参构造函数Date d2(2019,10,1);//调用带参构造函数//注意:如果无参构造函数创建对象时,对象后面不用跟括号,否则就变成了函数的声明。
}

如果类中没有显示定义构造函数,C++编译器会自动生成一个无参的默认构造函数,一旦用户显示定义,编译器将不再生成。
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。
构造函数调用之后,对象中已经有一个初始值,但不能将其称为类对象成员的初始化,构造函数体重的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。

初始化列表

初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个“成员变量”后面跟一个放在括号中的初始值或表达式。

class Date
{
public:Date(int year,int month,int day):_year(year),_month(month),_day(day){}
private:int _year;int _month;int _day;
};

注意:
1.每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
2.类中包含以下成员,必须放在初始化列表位置进行初始化:

  • 引用成员变量
  • const成员变量
  • 类类型成员(该类没有默认构造函数)

3.尽量初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
4.成员变量在类中的声明次序就是其在初始化列表中的初始化顺序,例如以下程序会出错:

class Array
{
public:Array(int size):_size(size),_array((int*)malloc(sizeof(int)*_size))//先执行{}
private:int* _array;int _size;
};

避免错的方法就是让声明的顺序和定义的顺序保持一致。

explicit关键字
构造函数不仅可以构造与初始化对象,对于单个参数的构造函数,还具有类型转换的作用

class Date
{
public:Date(int year):_year(year){}/*explicit Date(int year):_year(year){}*/
private:int _year;int _month;int _day;
};
void Test()
{Date d1(2019);d1=2018;//用一个整形变量给日期类对象赋值,实际编译器背后会用2018构造一个无名对象,最后用无名对象给d1对象进行赋值
}

上述代码其实可读性并不是很好,如果用explicit修饰构造函数,将会禁止单参构造函数的隐式转换。

2.析构函数

前面我们已经知道用一个类创建一个对象就要调用类的构造函数,那最后这个对象又是怎么没的呢?这就需要析构函数
但析构函数并不是完成对象的销毁,局部对象销毁工作由编译器完成的。对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。

析构函数的特征

  • 析构函数名在类名前加上字符~
  • 无参数和返回值
  • 一个类有且只有一个析构函数。若未显示定义,系统会自动生成默认的析构函数
  • 对象生命周期结束时,C++编译系统自动调用析构函数。
typedef int DataType;
class SeqList
{
public :SeqList (int capacity = 10){_pData = (DataType*)malloc(capacity * sizeof(DataType));assert(_pData);_size = 0;_capacity = capacity;}~SeqList(){if (_pData){free(_pData ); // 释放堆上的空间_pData = NULL; // 将指针置为空_capacity = 0;_size = 0;}}
private :int* _pData ;size_t _size;size_t _capacity;
};
//编译器生成的默认析构函数,对会自定类型成员调用它的析构函数。
class String
{
public:String(const char* str = "jack"){_str = (char*)malloc(strlen(str) + 1);strcpy(_str, str);}~String(){cout << "~String()" << endl;free(_str);}	
private:char* _str;
};
class Person
{
private:String _name;int _age;
};
int main()
{Person p;return 0;
}

3.拷贝构造函数

拷贝构造函数的特征:

  • 拷贝构造函数是构造函数的一个重载形式。
  • 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用
  • 若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷
    贝,这种拷贝我们叫做浅拷贝,或者值拷贝

在这里插入图片描述

Date(const Date& date)
{_year=date.year;_month=date.month;_day=date.day;
}

4.运算符重载

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字
operator后面接需要重载的运算符符号

函数原型:返回值类型 operator操作符(参数列表)
注意:

  • 不能通过连接其他符号来创建新的操作符:比如operator@。
  • 重载操作符必须有一个类类型或者枚举类型的操作数。
  • 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不 能改变其含义。
  • 作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的
    操作符有一个默认的形参this,限定为第一个形参。
  • .* 、:: 、sizeof 、?: 、. 注意以上5个运算符不能重载。

5. 赋值运算符重载

class Date
{
public :Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}Date (const Date& d){_year = d._year;_month = d._month;_day = d._day;}Date& operator=(const Date& d){if(this != &d){_year = d._year;_month = d._month;_day = d._day;}return *this;}
private:int _year ;int _month ;int _day ;
};

赋值运算符主要有四点:

  1. 参数类型
  2. 返回值
  3. 检测是否自己给自己赋值
  4. 返回*this

一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝。

6.取地址及const修饰的取地址操作符重载

这两个默认成员函数一般不用重新定义 ,编译器默认会生成

class Date
{
public :Date* operator&(){return this ;}const Date* operator&()const{return this ;}
private :int _year ; // 年int _month ; // 月int _day ; // 日
};

这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容。
结论:
5. const对象不能调用非const成员函数
6. 非const对象可以调用const成员函数
7. const成员函数内不能调用其它的非const成员函数
8. 非const成员函数内可以调用其它的const成员函数

总结6个默认成员函数

在这里插入图片描述

这篇关于细谈C++中的“类和对象”的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/809798

相关文章

【C++ Primer Plus习题】13.4

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream>#include "port.h"int main() {Port p1;Port p2("Abc", "Bcc", 30);std::cout <<

C++包装器

包装器 在 C++ 中,“包装器”通常指的是一种设计模式或编程技巧,用于封装其他代码或对象,使其更易于使用、管理或扩展。包装器的概念在编程中非常普遍,可以用于函数、类、库等多个方面。下面是几个常见的 “包装器” 类型: 1. 函数包装器 函数包装器用于封装一个或多个函数,使其接口更统一或更便于调用。例如,std::function 是一个通用的函数包装器,它可以存储任意可调用对象(函数、函数

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

06 C++Lambda表达式

lambda表达式的定义 没有显式模版形参的lambda表达式 [捕获] 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 有显式模版形参的lambda表达式 [捕获] <模版形参> 模版约束 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 含义 捕获:包含零个或者多个捕获符的逗号分隔列表 模板形参:用于泛型lambda提供个模板形参的名

6.1.数据结构-c/c++堆详解下篇(堆排序,TopK问题)

上篇:6.1.数据结构-c/c++模拟实现堆上篇(向下,上调整算法,建堆,增删数据)-CSDN博客 本章重点 1.使用堆来完成堆排序 2.使用堆解决TopK问题 目录 一.堆排序 1.1 思路 1.2 代码 1.3 简单测试 二.TopK问题 2.1 思路(求最小): 2.2 C语言代码(手写堆) 2.3 C++代码(使用优先级队列 priority_queue)

【C++高阶】C++类型转换全攻略:深入理解并高效应用

📝个人主页🌹:Eternity._ ⏩收录专栏⏪:C++ “ 登神长阶 ” 🤡往期回顾🤡:C++ 智能指针 🌹🌹期待您的关注 🌹🌹 ❀C++的类型转换 📒1. C语言中的类型转换📚2. C++强制类型转换⛰️static_cast🌞reinterpret_cast⭐const_cast🍁dynamic_cast 📜3. C++强制类型转换的原因📝

C++——stack、queue的实现及deque的介绍

目录 1.stack与queue的实现 1.1stack的实现  1.2 queue的实现 2.重温vector、list、stack、queue的介绍 2.1 STL标准库中stack和queue的底层结构  3.deque的简单介绍 3.1为什么选择deque作为stack和queue的底层默认容器  3.2 STL中对stack与queue的模拟实现 ①stack模拟实现

c++的初始化列表与const成员

初始化列表与const成员 const成员 使用const修饰的类、结构、联合的成员变量,在类对象创建完成前一定要初始化。 不能在构造函数中初始化const成员,因为执行构造函数时,类对象已经创建完成,只有类对象创建完成才能调用成员函数,构造函数虽然特殊但也是成员函数。 在定义const成员时进行初始化,该语法只有在C11语法标准下才支持。 初始化列表 在构造函数小括号后面,主要用于给

2024/9/8 c++ smart

1.通过自己编写的class来实现unique_ptr指针的功能 #include <iostream> using namespace std; template<class T> class unique_ptr { public:         //无参构造函数         unique_ptr();         //有参构造函数         unique_ptr(