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

C++类与对象

一、类与对象初阶

1.类与对象思想

面向对象的特点:

  • 封装、继承、多态

面向对象编程的特点:

  • 易维护:可读性高,即使改变了需求,由于继承的存在,只需要对局部模块进行修改,维护起来非常方便,维护的成本也比较低。
  • 质量高:可以重用以前项目中已经被测试过的类,使系统满足业务需求从而具有更高的质量
  • 效率高:在软件开发时,根据设计的需求要对现实世界的事物进行抽象,从而产生了类
  • 易扩展:由于继承、封装、多态的特性,可设计出高内聚、低耦合的系统结构,使系统更加灵活、更容易扩展,而且成本也比较低。

2.类与对象的概念

c++中的类可以看成c语言中的结构体的升级版,可以在里面定义函数。

类体中内容称为类的成员:类中的变量称为类的属性成员变量; 类中的函数称为类的方法或者成员函数

以下为两种定义类的方式:

struct classname
{//成员变量//成员函数
};class classname
{//成员变量//成员函数
};

两种方式的区别(后面会讲解):

  • struct:内部默认是公有权限,结构体外部可以访问其内部成员
  • class:内部默认是私有权限,类的外部不能直接访问内部成员(可以手动声明为公有权限)

关于类的几点说明:

  • 类的定义的最后有一个分号";",它是类的一部分,表示类定义结束,不能省略。
  • 一个类可以创建多个对象,每个对象都是一个变量
  • 类是一种构造类型,大小的计算方法和C语言中的struct一样,需要字节对齐
  • 类成员变量的访问方法:通过 .或者->来访问
  • 成员函数是类的一个成员,出现在类中,作用范围由类来决定;而普通函数是独立的,作用范围是全局或者某个命名空间

3.类中成员函数的定义

方法一:声明和定义全部放在类体中

注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。

方法二:类声明放在.h文件中,成员函数定义放在.cpp文件中,

注意:成员函数名前需要加:类名::

4.类的访问限定符

C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部使用

访问限定符说明:

  • public成员在类外可以直接访问
  • protected和private成员只能在类里访问,在类外不能访问,此处protected和private是类似的
  • protected和private的区别:子类继承后,protected成员可以被子类访问,但是private成员是不能被子类访问
  • 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止(如果后面没有访问限定符,作用域就到类结束)
  • class的默认访问权限为private,struct为public(因为struct要兼容C)

C++中struct和class的区别是什么?

        C++需要兼容C语言,所以C++中struct可以当成结构体使用。另外C++中struct还可以用来定义类。和class定义类是一样的,区别是struct定义的类默认访问权限是public,class定义的类默认访问权限是private。

5.类的作用域

类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 ::
作用域操作符指明成员属于哪个类域。

class Person
{
public:void PrintPersonInfo();
private:char _name[20];
};// 这里需要指定PrintPersonInfo是属于Person这个类域
void Person::PrintPersonInfo()
{cout << _name << endl;
}

6.类的实例化

用某个类的类型创建对象的过程,称为类的实例化

  • 类本身是没有空间的, 实例化出对象才会分配空间。类就好像建造图纸一样, 而对象才是房子
  • 一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量

7.类对象的大小

1)结构体内存对齐规则

  • 1.第一个成员直接对齐到相对于结构体变量起始位置为0的偏移处
  • 2.从第二个成员开始,要对齐到 对齐数 的整数倍的偏移处
  • 注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小 的较小值。
     VS中默认的对齐数为8
  • 3.结构体的大小为 最大对齐数 的整数倍
  • 注意:对齐数 = 编译器默认的一个对齐数 与 最大成员大小 的较小值。
  • 4.如果一个结构体中存在嵌套结构体,则嵌套结构体对齐到自己的最大对齐数的整数倍
  • 如:一个嵌套结构体大小为16字节,其最大对齐数为8,则对齐到8的倍数,往下占16个字节

#pragma pack(n)  设置编译器默认对齐数

2) 类对象的存储方式

实例化对象中只保存成员变量,成员函数存放在公共的代码段

这是因为成员函数是所有对象都调用的,如果每个实例化对象里面都存一份函数,非常浪费空间

注意:没有成员变量的类大小为一个字节,用来唯一标识这个类的对象

3)类对象的大小

类对象的大小计算规则与结构体类似

举例:

#include <iostream>
using namespace std;// 类中既有成员变量,又有成员函数
class A1 {
public:void f1() {}
private:int _a;
};
// 类中仅有成员函数
class A2 {
public:void f2() {}
};// 类中什么都没有---空类
class A3
{
};int main()
{cout << "sizeof(A1) = " << sizeof(A1) << endl;cout << "sizeof(A2) = " << sizeof(A2) << endl;cout << "sizeof(A3) = " << sizeof(A3) << endl;
}

8.this指针

1)this指针的引出

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

2)this指针的特性

  •  this指针的类型:类的类型* const。所以,在成员函数中,不能给this指针赋值。
  • 只能在“成员函数”的内部使用
  • this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
  • this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递

3)一些说明

问题1:this指针存在哪里?

this指针存在栈上,因为它是形参。

问题二:this指针可以为空吗?

先看下面这个例子:

二、类与对象进阶

1.类的六个默认成员函数

如果一个类中什么成员都没有,称为空类,但是空类中真的什么都没有吗?

并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。

默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

2.构造函数

概念:构造函数是一个特殊的成员函数名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次

作用:构造函数的主要任务并不是开空间创建对象,而是初始化类对象。

特征:

  • 函数名与类名相同。
  • 无返回值。
  • 对象实例化时编译器自动调用对应的构造函数。
  • 构造函数可以重载。
class Date
{
public:// 1.无参构造函数Date(){}// 2.带参构造函数Date(int year, int month, int day){_year = year;_month = month;_day = day;}
private:int _year;int _month;int _day;
};void TestDate()
{Date d1; // 调用无参构造函数Date d2(2015, 1, 1); // 调用带参的构造函数// 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明// 以下代码的函数:声明了d3函数,该函数无参,返回值为一个日期类型的对象Date d3();//函数的声明
}
  • 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数;一旦用户显式定义,编译器将不再生成。

编译器默认生成的构造函数:

1.对内置类型,可以在类中声明时给默认值,初始化时使用默认值;无默认值就不做处理

2.对自定义类型会调用它的默认构造函数

举例:

class Time
{
public:Time(){cout << "Time()" << endl;_hour = 0;_minute = 0;_second = 0;}
private:int _hour;int _minute;int _second;
};class Date
{
private://声明// 基本类型(内置类型)int _year = 2025;//给出默认值int _month;int _day;// 自定义类型Time _t;
};int main()
{Date d;return 0;
}

Date类的对象d调用构造函数时,会调用Time的构造函数初始化 自定义类型_t ,内置类型_year使用声明时的默认值2025进行初始化,而_month和_day则不作处理,不初始化。

  • 无参/全缺省/编译器默认生成的构造函数,都是默认构造函数,并且默认构造函数只能有一个
class Date
{
public://无参的构造函数Date(){_year = 1900;_month = 1;_day = 1;}//全缺省的构造函数Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}
private:int _year;int _month;int _day;
};

3.析构函数

概念:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作

特性:

  • 析构函数名是在类名前加上字符 ~。
  • 无参数无返回值类型。
  • 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
  • 析构函数不能重载
  • 对象生命周期结束时,C++编译系统系统自动调用析构函数。

编译器默认生成的析构函数:

1.对内置类型不做处理

2.对自定义类型去调用它的析构函数

(与默认构造函数类似)

class Stack
{
public://构造函数Stack(size_t capacity = 3){_array = (int*)malloc(sizeof(DataType) * capacity);if (NULL == _array){perror("malloc申请空间失败!!!");return;}_capacity = capacity;_size = 0;}//析构函数~Stack(){if (_array){free(_array);_array = NULL;_capacity = 0;_size = 0;}}
private:int* _array;int _capacity;int _size;
};

4.拷贝构造函数

概念:拷贝构造函数只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用

特征:

  • 拷贝构造函数是构造函数的一个重载形式
  • 拷贝构造函数的参数只有一个且必须是类类型对象的引用。使用传值方式编译器直接报错,因为传给形参的过程也是构造拷贝,会引发无穷递归调用
  • 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝/值拷贝。

编译器生成的默认拷贝构造函数:

1.内置类型是按照字节方式直接拷贝的

2.自定义类型是调用其拷贝构造函数完成拷贝

编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?

例如:如果类中的成员变量有指针,在构造拷贝给另一个对象后,两个对象中的指针会指向同一块资源。销毁时,同一块内存空间释放两次,会造成程序崩溃

所以,类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。

class Time
{
public:Time(){_hour = 1;_minute = 1;_second = 1;}//拷贝构造函数Time(const Time& t){_hour = t._hour;_minute = t._minute;_second = t._second;cout << "Time::Time(const Time&)" << endl;}
private:int _hour;int _minute;int _second;
};class Date
{
private:// 基本类型(内置类型)int _year = 1970;int _month = 1;int _day = 1;// 自定义类型Time _t;
};int main()
{Date d1;// 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数// 但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数Date d2(d1);return 0;
}

拷贝构造函数典型调用场景:

  • 使用已存在对象创建新对象
  • 函数参数类型为类类型对象
  • 函数返回值类型为类类型对象

5.赋值运算符重载

1)运算符重载

C++为了增强代码的可读性引入了运算符重载

运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

函数结构:返回值类型 operator操作符(参数列表)

例如:

Date是自定义类型,原本不能和int类型相加,但经过运算符重载后就可以。

Date operator+(int days)
{...}

注意:

  • 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this指针
  • .*  ::  sizeof  ?:  .  这5个运算符不能重载

2)赋值运算符重载

赋值运算符重载就是 = ,就是两个相同类型的对象赋值

(1)赋值运算符重载格式:

  • 参数类型:const T&,传递引用可以提高传参效率
  • 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
  • 返回*this :要连续赋值(例如:a=b=c)
class Date
{
public ://赋值运算符重载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 ;
};

(2)用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝

注意:与拷贝构造函数相似,如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。

(3)赋值运算符只能重载成类的成员函数,不能重载成全局函数

原因:如果在类中不显式实现赋值运算符重载,编译器会生成一个默认的。此时,用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值
运算符重载只能是类的成员函数

错误示范:会编译失败!!!

class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}int _year;int _month;int _day;
};// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
{if (&left != &right){left._year = right._year;left._month = right._month;left._day = right._day;}return left;
}
// 编译失败:
// error C2801: “operator =”必须是非静态成员

3)前置++和后置++重载

要实现++重载:返回值 operator++();

那如何区分前置后置呢??

C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递

class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}
// 前置++:返回+1之后的结果
// 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率Date& operator++(){_day += 1;return *this;}
// 后置++:
// 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存一份,然后给this+1
// 而temp是临时对象,因此只能以值的方式返回,不能返回引用Date operator++(int){Date temp(*this);_day += 1;return temp;}
private:int _year;int _month;int _day;
};int main()
{Date d;Date d1(2022, 1, 13);d = d1++; // d: 2022,1,13 d1:2022,1,14d = ++d1; // d: 2022,1,15 d1:2022,1,15return 0;
}

PS:可以看到前置++直接改变实参,并且传引用返回;后置++需要创建临时变量,并且传值返回,这就需要拷贝,降低了效率。所以,尽量使用前置++

6.取地址以及const取地址操作符重载

1)const成员

将const修饰的“成员函数”称之为const成员函数。

想使函数内部不能修改类成员,就需要用 const修饰*this 。但是this指针不能显示传入函数,所以,在函数定义或者声明的括号后面加上const,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改

例如:

Const对象能否调用非const成员函数?

不能。因为 const对象 只有 const this指针 ,而 非const成员函数 期望传入 非const this指针 ,导致类型不匹配。

非const对象能否调用const成员函数?

能。 非const对象 隐含一个 非const this指针 ,而 const成员函数 要求传入const this指针。(const参数可以接受非const值)

Const成员函数内能否调用其他非const成员函数?

不能。因为 const成员函数 只有 const this指针 ,调用 非const成员函数 需要 非const this指针 ,这会导致错误。

非const成员函数内能否调用其他const成员函数?

能。因为 非const this指针 可以传递给 const成员函数 (const成员函数接受const指针)。

2)取地址及const取地址操作符重载

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

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

7.初始化列表

初始化列表主要用来给对象中的特殊成员初始化

注意:

  • 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
  • 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后
    次序无关
  • 类中包含以下成员,必须放在初始化列表位置进行初始化:引用成员变量,const成员变量,自定义类型成员(且该类没有 无参/全缺省/默认构造函数 时)

例如:

class A
{
public:A(int a):_a(a){}
private:int _a;
};
class B
{
public:B(int a, int ref):_aobj(a),_ref(ref),_n(10){}
private:A _aobj; // 没有默认构造函数int& _ref; // 引用const int _n; // const
};

8.explicit

构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值
的构造函数,还具有类型转换的作用

用explicit修饰构造函数,将会禁止构造函数的隐式转换。

9.static成员

声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用
static修饰成员函数,称之为静态成员函数

静态成员变量一定要在类外进行初始化

class A
{
public:A() { ++_scount; }A(const A& t) { ++_scount; }~A() { --_scount; }static int GetACount() { return _scount; }
private:static int _scount;//类中声明
};int A::_scount = 0;//类外定义

特性:

  • 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
  • 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
  • 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
  • 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
  • 静态成员也是类的成员,受public、protected、private 访问限定符的限制

静态成员函数可以调用非静态成员函数吗?

不能直接调用。静态成员函数没有this指针,而非静态成员函数隐式依赖this指针访问成员变量/函数。

非静态成员函数可以调用类的静态成员函数吗?

可以直接调用。静态成员函数属于类而非对象,调用时无需this指针。

10.友元

友元分为:友元函数友元类

友元是一种很有效的方式,但是并不推荐使用,因为它会破坏封装。

1)友元函数:

友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在
类的内部声明,声明时需要加friend关键字

class A
{friend void print(const A& a);//类中声明友元
private:int _n;
};void print(const A& a)
{printf("%d\n", a._n);//在类外可以访问私有成员
}

说明:

  • 友元函数可访问类的私有和保护成员,但不是类的成员函数
  • 友元函数不能用const修饰
  • 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
  • 一个函数可以是多个类的友元函数
  • 友元函数的调用与普通函数的调用原理相同

2)利用友元重载<<和>>:

问题:为什么不直接将operator<<重载成成员函数呢?

原因:在类中无法实现。因为,在类的函数中,隐含的this指针占据第一个参数的位置。this指针指向的参数是左操作数,但是实际使用中,对象应为右操作数。所以,要将operator<<重载成全局函数。但这又会导致类外没办法访问成员,此时就需要友元来解决。

operator>>同理。

class Date
{friend ostream& operator<<(ostream& _cout, const Date& d);friend istream& operator>>(istream& _cin, Date& d);
public:Date(int year = 1900, int month = 1, int day = 1): _year(year), _month(month), _day(day){}
private:int _year;int _month;int _day;
};ostream& operator<<(ostream& _cout, const Date& d)
{_cout << d._year << "-" << d._month << "-" << d._day;return _cout;
}
istream& operator>>(istream& _cin, Date& d)
{_cin >> d._year;_cin >> d._month;_cin >> d._day;return _cin;
}int main()
{Date d;cin >> d;cout << d << endl;return 0;
}

3)友元类:

  • 友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
  • 友元关系是单向的,不具有交换性。
  • 友元关系不能传递,如果C是B的友元, B是A的友元,则不能说明C时A的友元
class A
{friend class B;//类中声明友元
private:int _n;
};class B
{void Printf(const A& a){printf("%d", a._n);//另一个类可以访问}
};

11.内部类

概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,
它不属于外部类,外部类的对象无法访问内部类的私有成员。

注意:

  • 内部类是外部类的友元类但是外部类不是内部类的友元
  • 内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
  • sizeof(外部类)=外部类,和内部类没有任何关系。
class A
{
private:static int k;int h;
public:class B // B天生就是A的友元{public:void foo(const A& a){cout << k << endl;cout << a.h << endl;}};
};
int A::k = 1;int main()
{A::B b;b.foo(A());return 0;
}

12.匿名对象

匿名对象的生命周期只有一行,下一行他就会自动调用析构函数

作用:通过定义一个临时对象,调用类的成员函数

class A
{
public:A(int a = 0):_a(a){}print(){cout<<_a<<endl;}
private:int _a;
};int main()
{A aa1;A aa2(2);// 不能这么定义对象,因为编译器无法识别下面是一个函数声明,还是对象定义//A aa1();A();//匿名对象A().print();// 匿名对象调用函数return 0;
}

13.拷贝对象时的一些编译器优化

在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝

class A
{
public:A(int a = 0)//构造函数:_a(a){}A(const A& aa)//拷贝构造函数:_a(aa._a){}A& operator=(const A& aa)//赋值运算符重载{if (this != &aa){_a = aa._a;}return *this;}private:int _a;
};void f1(A aa)
{}
A f2()
{A aa;return aa;
}int main()
{// 传值传参A aa1;f1(aa1);// 传值返回f2();// 隐式类型,构造+拷贝构造->优化为直接构造f1(1);// 一个表达式中,构造+拷贝构造->优化为一个构造f1(A(2));// 一个表达式中,拷贝构造+拷贝构造->优化一个拷贝构造A aa2 = f2();// 一个表达式中,拷贝构造+赋值重载->无法优化aa1 = f2();return 0;
}
http://www.dtcms.com/a/393358.html

相关文章:

  • 企业级Docker镜像仓库Harbor
  • ESD防护设计宝典(七):生命线的秩序——关键信号线布线规则
  • 【ROS2】Beginner : CLI tools - 理解 ROS 2 话题
  • RL知识回顾
  • Java多线程编程指南
  • 【论文速读】基于地面激光扫描(TLS)和迭 代最近点(ICP)算法的土坝监测变形分析
  • GAMES101:现代计算机图形学入门(Chapter2 向量与线性代数)迅猛式学线性代数学习笔记
  • 汉语构词智慧:从历史优势到现实考量——兼论“汉语全面改造英语”的可能性
  • 仿tcmalloc高并发内存池
  • 墨者学院-通关攻略(持续更新持续改进)
  • 10厘米钢板矫平机:把“波浪”压成“镜面”的科学
  • ESP32- 项目应用1 智能手表之网络配置 #6
  • TCP/IP 互联网的真相:空间域和时间域的统计学
  • 同步与异步
  • C++中char与string的终极对比指南
  • Java基础 9.20
  • U228721 反转单链表
  • 串行总线、并行总线
  • `HTML`实体插入软连字符: `shy;`
  • 日志驱动切换针对海外vps日志收集的操作标准
  • Zynq开发实践(SDK之自定义IP2 - FPGA验证)
  • 广东电信RTSP单播源参数解析
  • 关于工作中AI Coding的一些踩坑经验
  • MyBatis 参数传递详解:从基础到复杂场景全解析
  • ego(8)---L-BFGS优化算法与B样条生成最终轨迹
  • 【开题答辩全过程】以 HPV疫苗预约网站为例,包含答辩的问题和答案
  • Linux网络中Socket网络套接字的高级应用与优化策略
  • 人才测评系统选型参考:含国内平台对比
  • 人才素质测评在线测评系统平台清单:5款推荐
  • 【语法进阶】匹配分组