当前位置:   article > 正文

【C++】类和对象_c++对象的引用

c++对象的引用

面向对象和面向过程

举个例子:

淘宝这个软件,我要买一个东西,就应该有以下几步:

上架商品-》下单-》送快递-》收快递-》评价

C语言关注的就是这个过程

而C++关注的更多是对象:

平台-》快递-》用户

这就是面向对象,将上面繁琐的过程总结为三个对象

这样再来看解释是不是会更好理解:

C 语言是 面向过程 的, 关注 的是 过程 ,分析出求解问题的步骤,通过函数调用逐步解决问题。
C++ 基于面向对象 的, 关注 的是 对象 ,将一件事情拆分成不同的对象,靠对象之间的交互完成。

类的引用

既然C++是靠对象交互实现的功能,那类,就是必须要掌握的知识,在C语言里面,结构体一般是这样的:

里面定义的都是变量,但是在C++里面就不是这样的,C++不仅可以定义变量还可以定义函数,就好像下面:

 这个结构体不仅有对象,还有函数,那它的使用就是这样:

 先定义一个对象,随后调用这个对象函数。

这个结构体也可以用class来定义,区别下面会提到。

类的定义 

类的定义跟结构体很像,它的格式是这样的:

类中的元素称为类的成员:类中的数据称为类的属性或者成员变量; 类中的函数称为类的方法或者成员函数 

类有两种定义,第一种是先在头文件里面声明,再在.c文件里面实现,另一种是全部写上,这种全部写在类里面的函数会被当成内联函数,要注意 

第一种:

在头文件里面声明show函数

在.c里面定义函数,这里要注意,在函数名前面一定要加上::表面它属于这个类,否则它会找不到参数。 

下面就是第二种全部写上:

 那这里多出来一个public,这是什么意思?

类的访问限定符及封装

访问限定符

这里要说一个概念就是访问限定符,当定义出来一个类之后,它会默认有三种权限:

public(公有)

protecte(保护)

private(隐私)

---

公有的意思就是在类的外面也可以使用,保护和隐私相反。

访问限定符它会持续到下一个访问限定符的出现或者是类结束。

class默认是protected,而struct则是public。

这些了解一下就可以了,不用想太深。

这里还有个问题:

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

因为C++兼容C,所以struct在C++中同样可以创建类,class也可以,而struct的默认权限是public,而class则是protected。

---

封装

面向对象的三大特性:封装、继承、多态

但是在这个阶段,先认识一下封装,封装的本质,是将一些对象的实现方式和参数隐藏,只对外面实现接口调用。

在创建类的时候那个class就包含这一点,因为class创建的类它里面成员的默认属性是私有,因为是私有所以在类外面是访问不到的,而struct则是公开。

类的作用域

当写C++的时候,总会看到这么一句话:

假设是这样,我在一个头文件里面新建了一个类,这个类里面有一个函数,这个函数代码很长,可能有十几行,那我就可以这么写:

我在头文件里面声明,但是在.c里面实现,不过要在实现的函数名前面带上作用域,表示这个函数实现的是这个对象里面的 

 类的实例化、计算大小、存储方式

在说之前,先看一下正式的解释:

1. 类只是 一个 模型 一样的东西,限定了类有哪些成员,定义出一个类 并没有分配实际的内存空间 来存储它
2. 一个类可以实例化出多个对象, 实例化出的对象 占用实际的物理空间,存储类成员变量
3. 做个比方。 类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图 ,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间

举个例子,我这里创建了一个日期类,里面有这样的几个参数:

那这里提一个问,这三个对象有没有占用空间?

没有,因为它只是声明有这样的三个参数,还没有真的给初始化出来,所以不占用内存空间。

但如果我们要计算一个类要占用的内存空间,我们该如何计算?

就好像在C语言里面的,算一个结构体的大小一样,别忘了内存对齐,这里就是

4*3 = 12个字节。

类里面的函数占不占空间?

不占,举这样的一个例子,我这里创建了两个对象,一个d1,一个d2:

它们肯定是两个不同的对象,里面的参数也不一样,但是它们调用的函数,是不是这个类里面的同一个函数?

是的,所以为了优化,类里面的函数都会放到单独的一块空间,这块空间是公用的,所以计算的时候不算这一块。

假设这个类里面什么都没有,占用多少个字节? 

 

一个字节,虽然这个类里面什么都没有 ,但编译器在运行的过程中还是会象征的给它一个字节表示这是一个类。

this指针

这个,其实不是很好讲,这里我用个例子来说:

首先,我定义了一个对象,这个对象很简单,一个构造函数一个打印函数,随后,我在主函数里面初始化了a并将其赋值为2,随后我调用打印函数,在屏幕上打印了一个2.

为什么这里编译器会打印a的2,而不是一些随机值?

这里就要说到this指针,当写出如上代码的时候,看上其PrintA里面什么参数也没有,实际上,它隐藏的传了一个指针过去,这个指针称为this,*this就是a的地址,所以当我们看到上面的代码的时候,实际上它是这样的:

这样它在找的时候就能知道该打印哪个,我们还可以通过别的方法看到this指针:

 可以看到,我在类里面打印了this指针的地址,然后在外面打印了a的地址,发现它们两个的地址一样,可以说明确实是传了过去。

--------

默认成员函数

如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情况下,都会自动生成下面6 个默认成员函数。

以我要写的一个对象为例:

  1. class Data
  2. {
  3. };

这个里面看起来是空的,但实际上,它里面会包含以下几个成员函数:

负责初始化和清理的:构造函数、析构函数

负责拷贝赋值的:拷贝构造、赋值重载

负责取地址重载的:普通对象和const对象取地址,但这两个很少用,这里就不考虑

一个一个来,从构造函数开始:

构造函数

假设我现在写一个简单的对象,日期类:

  1. #include<iostream>
  2. using namespace std;
  3. class Date
  4. {
  5. public:
  6. void InitDate(int year, int month, int day)
  7. {
  8. _year = year;
  9. _month = month;
  10. _day = day;
  11. }
  12. void Display()
  13. {
  14. cout << _year << "-" << _month << "-" << _day << endl;
  15. }
  16. private:
  17. int _year;
  18. int _month;
  19. int _day;
  20. };

这是很简单的一个对象,里面有三个隐私变量的声明,而在变量之上有两个函数。

那根据上面的对象,我们定义对象应该是这样的:

不太好用,对吧?

我们知道整形可以 int a = 10;这样定义的时候就给赋值,那Date这样的类能不能也这样呢?

当然可以,这里就要用到构造函数,这里先看以下构造函数的定义:

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

 再看一下构造函数的特性:

构造函数 是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,但是需要注意的是构造函数的主要任务并不是开空间创建对象,而是初始化对象
其特征如下:
1. 函数名与类名相同。
2. 无返回值。
3. 对象实例化时编译器 自动调用 对应的构造函数。
4. 构造函数可以重载。

 看起来有点麻烦,但实际用起来还是比较简单的,首先,它的函数名跟类名一样,不需要返回值,类里面可以进行初始化的赋值。

这里就示范一个:

 可以看到,这个跟函数有点像的就是构造,它有三个参数,分别对应我们类里面的三个参数,我们在定义类的时候编译器会自己调用这个构造:

这里再运行看看:

对象就可以这么写了,初始化的赋值也没问题。

除此之外,还有两种:

不给参数,无参的

编译器自己生成的

顺着来看看,先看无参的:

它就可以这么写:

不给参数,直接赋值,如果需要直接赋值的话,那还是不要用这种:

再来看看编译器自己生成的,如果我们不写:

直接就随机值了,这里要说的就是,编译器不会对内置类型处理,比方说int,double这种,它默认构造之后还是一个随机值。

还是别用,这里我们主要了解是是第一个,给参数的,但是我们可以这么写:

调用的时候不给参数,它就会在这里的缺省值。

 注意一点,构造函数优先写全缺省的,别写了一个缺省之后再写一个不带参的,会报错

如果遇到了自定义类型,编译器会调用自定义类型的改造函数,有点类似于套娃,这里看下面的代码:

  1. class Time
  2. {
  3. public:
  4. Time()
  5. {
  6. cout << "Time()" << endl;
  7. _hour = 0;
  8. _minute = 0;
  9. _second = 0;
  10. }
  11. private:
  12. int _hour;
  13. int _minute;
  14. int _second;
  15. };
  16. class Date
  17. {
  18. private:
  19. // 基本类型(内置类型)
  20. int _year;
  21. int _month;
  22. int _day;
  23. // 自定义类型
  24. Time _t;
  25. };
  26. int main()
  27. {
  28. Date d;
  29. return 0;
  30. }

 Date里面有一个自定义类型Time,而当我们定义d这个对象的时候,编译器去找默认构造,发现Date里面的前三个是内置类型,所以它不会做处理,而_t则是自定义类型,那它就会在Time里面去找默认构造,看上面的代码是可以看到Time的默认构造是这个:

  1. Time()
  2. {
  3. cout << "Time()" << endl;
  4. _hour = 0;
  5. _minute = 0;
  6. _second = 0;
  7. }

在默认构造里面,打印了一行话,然后我们运行:

它也确实调用了这个默认构造 。

------

析构函数

这么说呢,这个其实理解起来跟构造函数是很像的,同样,我们先来看看它的盖帘和特性:

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

特征 

特征 如下:
1. 析构函数名是在类名前加上字符 ~
2. 无参数无返回值。
3. 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
4. 对象生命周期结束时, C++ 编译系统系统自动调用析构函数。

这个理解起来可以理解为是默认构造的翻版,它在这个对象生命周期到了的时候会自动调用,这里要注意的是它跟默认构造一样,它的析构同样不会对内置类型处理,而对自定义类型调用对应的析构函数。

如果,在这个对象里面有指针,那编译器也是不会处理的,所以如果写了指针,这个析构函数也是需要自己写的。

同时,如果一个类里面要自己写析构函数,那拷贝构造和拷贝赋值也是需要自己写的,原因之后再说,这里了解一下就好了。

------

拷贝构造函数

当我们用构造函数的时候,有没有可能,我们有的时候需要拿一个对象初始化另一个对象,那我们就需要用到拷贝构造函数。

构造函数 只有单个形参 ,该形参是对本 类类型对象的引用 ( 一般常用 const 修饰 ) ,在用 已存在的类类型对象 创建新对象时由编译器自动调用。
拷贝构造函数也是特殊的成员函数,其 特征 如下:
1. 拷贝构造函数 是构造函数的一个重载形式
2. 拷贝构造函数的 参数只有一个 必须使用引用传参 ,使用 传值方式会引发无穷递归调用

 这个说来也很简单,看看下面的例子:

这看是一个很简单的拷贝构造,需要的参数是一个同类型的对象,同样不需要返回值,只需要很简答的把对方的值给自己就好了。

不过注意的是特征的第二条,至于为什么会无穷递归,是因为形参是实参的临时拷贝,拷贝就要用到拷贝构造,拷贝构造又传过去参数...

死循环,注意一下就好了。

拷贝同样有两种,第一种:

浅拷贝,一个字节一个字节的拷贝,对于一些简单的int或者是double,很简单,但是如果有指针类型,浅拷贝也会把这个地址拷过去,也就是说,这两个指针指向的是同一块空间,都学到这个地步了,这个后果我就不多说了。

深拷贝:为了解决指针的这种类似问题,就出现了深拷贝,但我们现在也用不到。

这里还有一点要注意:

如果没有写拷贝函数,那编译器自动生成的拷贝构造是浅拷贝。

-----------
运算符的重载

 我们知道,同一种类型的对象是可以赋值的,那类也不例外,但是我们总不可能直接写:

int a = 10;

Data.a = a;

首先,类的成员我们是私有的,其次这两类型都不一样,所以要写也是这么写:

Data a(xxx);

Data b;

b = a;

为了实现这样的操作(指直接用等于),我们就需要这个,运算符的重载:

我先写一个示范:

operator就是关键,其后面跟着的=号是你要实现的运算符,然后后面是需要的参数,单目操作符就不需要传参,双目就要一个参数...

其次,还要考虑的是有连续赋值的可能,所以返回值可以考虑用类型的引用。

 其他的类似实现的操作符在下面的日期类会依次实现。

---------

const成员

这个说起来比较麻烦,所以先看代码:

  1. class Date
  2. {
  3. public :
  4. void Display ()
  5. {
  6. cout<<"Display ()" <<endl;
  7. cout<<"year:" <<_year<< endl;
  8. cout<<"month:" <<_month<< endl;
  9. cout<<"day:" <<_day<< endl<<endl ;
  10. }
  11. private :
  12. int _year ; //
  13. int _month ; //
  14. int _day ; //
  15. };
  16. void Test ()
  17. {
  18. Date d1 ;
  19. d1.Display ();
  20. const Date d2;
  21. d2.Display ();
  22. }

d1调用d1.Display肯定是没有问题的,但是d2呢?

如果运行程序,就会出现这样的错误:

因为d2是被const修饰的对象,但如果调用Dsiplay的话,函数隐藏的接受的对象类型是不会附带const的,这就设计到之前C++的基础知识。

const的对象只有const对象才能接收,如果是普通类型就会设计到一个权限放大的错误。

为了解决这个问题,所以C++就会有这样的规定:

在函数的括号后面加一个const:

这样不管是普通对象类型还是被const修饰的都可以调用这个函数了。

 ---

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

这么说呢,这个操作也是类似的,这里简单写一下:

  1. class Date
  2. {
  3. public :
  4. Date* operator&()
  5. {
  6. return this ;
  7. }
  8. const Date* operator&()const
  9. {
  10. return this ;
  11. }
  12. private :
  13. int _year ; // 年
  14. int _month ; // 月
  15. int _day ; // 日
  16. };

大概就是这样类似的操作...

-----------

日期类的实现

【C++】日期类_ClaudineKnous的博客-CSDN博客

看完了这个就可以去看看这个了,日期类的实现几乎概括了上面的所有知识。

-----------

这是后期的更新,一些补充的知识:

构造函数体的赋值

我们知道构造函数赋值的时候应该是这样:

  1. class Date
  2. {
  3. public:
  4. Date(int year, int month, int day)
  5. {
  6. _year = year;
  7. _month = month;
  8. _day = day;
  9. }
  10. private:
  11. int _year;
  12. int _month;
  13. int _day;
  14. };

虽然调用之后_year _month _day里面都有了一个初始值,但这个操作却并不能称之为初始化,因为里面的式子是:_year = year,=我们都知道是赋值的意思,也就是说在进入花括号之前_year就已经初始化了。

初始化只能初始化一次。但却可以多次赋值

假设有一个变量,它是const修饰的,我们知道const修饰的词是不能在初始化之后再赋值的,所以它唯一的办法就是在初始化的时候赋值,但如果调用上面的构造函数,那const的值无法修改就会出现错误。

为了解决这种变量必须在初始的时候赋值的问题,这里就有一种办法,叫做:初始化列表

它是这么写的:

  1. class Date
  2. {
  3. public:
  4. Date(int year, int month, int day)
  5. : _year(year)
  6. , _month(month)
  7. , _day(day)
  8. {}
  9. private:
  10. int _year;
  11. int _month;
  12. int _day;
  13. };

可以看到在构造函数的花括号之前就已经写了一串代码,这里的意思就是在初始化的时候将括号里面的值给到前面的对象,注意中间是逗号,最后也不要加分号,如果加上了就说明这行语句结束了,会报错。

这里还有几点要注意:

1. 每个成员变量在初始化列表中 只能出现一次 ( 初始化只能初始化一次 )
2. 类中包含以下成员,必须放在初始化列表位置进行初始化:
引用成员变量
const成员变量
自定义类型成员( 该类没有默认构造函数 )

 前面都很好理解,但最后一句:自定义类型成员(该类没有默认构造函数)怎么理解,这里看代码:

 可以看到这里A里面明明有一个构造函数,但却提醒说没有构造函数,这是一个在进入花括号里面之后_aobj是已经初始化完的状态,在里面的操作仅仅是赋值,所以这里在初始化会报错,正确办法应该是在进入花括号之前就把值给它初始化:

 总之,就是能用初始化队列的就用初始化队列,包括自定义类型。

看下面代码:

可以看到对于_t它是完成了初始化的,并且它也执行了花括号里面的内容。

-----

 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关

看下面的代码,

  1. class A {
  2. public:
  3. A(int a)
  4. :_a1(a)
  5. , _a2(_a1)
  6. {}
  7. void Print() {
  8. cout << _a1 << " " << _a2 << endl;
  9. }
  10. private:
  11. int _a2;
  12. int _a1;
  13. };
  14. int main()
  15. {
  16. A a(1);
  17. a.Print();
  18. return 0;
  19. }

这里会发生什么?

先捋一遍思路,首先传过去了一个1,在调用构造函数的时候这个2先给到了_a1,然后_a1的值再给了_a2,按理来说,两个打印的值都应该是1,但实际上呢?

 可以看到,_a1的值是1,但是_a2的值却是随机值,这就是上面所说的,会按照声明顺序进行初始化。

因为_a2声明是_a1前面,所以在初始化的时候_a2会先初始化,_a2的初始化是_a1的拷贝,但此时_a1还没有完成初始化的赋值,所以就把一个随机值拷贝了过去,然后_a1才被初始化为1.

-------

static成员

在C语言里面我们就知道static是静态变量,作用域是全局,在C++里面同样也可以使用,但却有一点不一样。

先看看概念:

声明为 static 的类成员 称为 类的静态成员 ,用 static 修饰的 成员变量 ,称之为 静态成员变量 ;用 static 修饰 成员函数 ,称之为 静态成员函数 静态的成员变量一定要在类外进行初始化

简单写个代码方便理解:

这里,我的_a2是被static修饰的,如果在构造函数初始化队列里面进行初始化会出现如上图一样的错误,正确方式是:

 

在类外面 是,首先声明这是个什么类型,然后是这个对象属于哪个类,其次再赋值。

这里要注意对象的几点特性:

1. 静态成员 为所有 类对象所共享 ,不属于某个具体的实例
2. 静态成员变量 必须在 类外定义 ,定义时不添加 static 关键字
3. 类静态成员即可用类名 :: 静态成员或者对象 . 静态成员来访问
4. 静态成员函数 没有 隐藏的 this 指针 ,不能访问任何非静态成员
5. 静态成员和类的普通成员一样,也有 public protected private3 种访问级别,也可以具有返回值

 这里再提出两个问题:

静态成员函数可不可以调用非静态成员函数,反之呢?

答:

静态的不能调用非静态的,但是非静态的可以调用静态的。

------

友元

友元分为两种:友元函数和友元类

友元提供了一种突破封装的方式,但也正是因为突破了封装,导致代码的耦合性变差了。

友元类

  1. class Time;
  2. class Date
  3. {
  4. friend Time;
  5. public:
  6. Date(int year = 1, int month = 1, int day = 1)
  7. :_year(year),
  8. _month(month),
  9. _day(day)
  10. {}
  11. private:
  12. int _year;
  13. int _month;
  14. int _day;
  15. };
  16. class Time
  17. {
  18. public:
  19. Time(int h = 1,int min = 1,int s = 1)
  20. :_h(h),
  21. _min(min),
  22. _s(s)
  23. {}
  24. void printf(Date& d) const
  25. {
  26. cout << d._year << "-" << d._month << "-" << d._day << "-" << this->_h << "-" << this->_min << "-" << this->_s << endl;
  27. }
  28. private:
  29. int _h;
  30. int _min;
  31. int _s;
  32. };

这就是一个友元类。

首先有两个类,第一个是日期,第二个是时间,我想在时间类里面顺带着一起打印日期,那我就要在时间类里面加上 friend Time,表示Time是Date的友元,那Time就可以访问到Date里面的私有数据。

但是反过来,Date不是Time的友元,所以Date无法访问到Time里面的对象。

友元是单向性的

友元没有传递性如果BA的友元,CB的友元,则不能说明CA的友元

--

友元函数

如果知道了友元类,那友元函数就轻松多了。

这里还是看代码演示比较好:

  1. class Date
  2. {
  3. friend void Printf(const Date& d);
  4. public:
  5. Date(int year = 1, int month = 1, int day = 1)
  6. :_year(year),
  7. _month(month),
  8. _day(day)
  9. {}
  10. private:
  11. int _year;
  12. int _month;
  13. int _day;
  14. };
  15. void Printf(const Date& d)
  16. {
  17. cout << d._year << "-" << d._month << '-' << d._day << endl;
  18. }

在Dtae里面声明有Printf这个函数,格式是:返回值 函数名(函数参数)

这样就可以在类外面访问类的私有成员。

这里要注意几点:

友元函数可以访问类的私有和保护乘员,但是不是类的成员函数

友元函数不能用const修饰

友元函数可以定义在类里面的任何地方,不受访问限定符的限制,但最好定义在类的最前面

一个函数可以是多个类的友元函数

友元函数的调用和普通函数的调用原理相同

 ---

内部类

这个比较简单,先看概念和特征:

概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。注意此时这个内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去调用内部类。外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的友元类。注意友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
---
特性:
1. 内部类可以定义在外部类的 public protected private 都是可以的。
2. 注意内部类可以直接访问外部类中的 static 、枚举成员,不需要外部类的对象 / 类名。
3. sizeof( 外部类 )= 外部类,和内部类没有任何关系

然后,我们看一段代码演示:

  1. class A {
  2. private:
  3. static int k;
  4. int h;
  5. public:
  6. class B
  7. {
  8. public:
  9. void foo(const A& a)
  10. {
  11. cout << k << endl;//OK
  12. cout << a.h << endl;//OK
  13. }
  14. };
  15. };

 这个给对象的名字是A,但是在A的里面有有一个对象叫B,我在B里面的函数要访问A的私有成员,运行结果如图:

可以直接访问,不受权限的限制 。

--------

thank watch

因为最近有考试,所以更新放慢

本文内容由网友自发贡献,转载请注明出处:https://www.wpsshop.cn/w/weixin_40725706/article/detail/244289
推荐阅读
相关标签
  

闽ICP备14008679号