【C++】类和对象(上)

目录

1.面向过程和面向对象

2.类的引入 

3.类的定义

4.类的访问限定符及封装

4.1访问限定符

4.2封装

5.类的作用域 

6.类的实例化 

7.类对象模型 

7.1计算类对象的大小 

7.2类对象的存储方法 

7.3结构体内存对齐规则 

8.this指针  

8.1this指针的引用 

8.2this指针的特性 

9.类的6个默认成员函数

10.构造函数

10.1概念 

10.2特性

11.析构函数

11.1概念

11.2特性


1.面向过程和面向对象

C语言是面向过程的,关注的是过程,分析出求解问题的步骤,再通过调用函数逐步解决问题 

C++是基于面向对象的,关注的是对象,将一件事情分成不同的几个对象,靠对象之间的交互完成 


2.类的引入 

C语言结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。比如:之前在数据结构初阶中,用C语言方式实现的栈,结构体中只能定义变量;现在以C++方式实现,会发现struct中也可以定义函数


3.类的定义

class className
{
        // 类体:由成员函数和成员变量组成
};      // 一定要注意后面的分号 

class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。

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

类的两种定义方式
1. 声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。

2. 类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名:: 


一般情况下第二种方式更优,在平时练习为了方便可以采用第一种,但是在以后或者做项目建议是使用第二种


4.类的访问限定符及封装

4.1访问限定符

C++实现封装的方式:用类将对象的属性与方法结合在一起,让对象更加完善,通过访问权限选择性地将接口提供给外部的用户使用,这样可以防止其他人恶意修改程序的数据以及防止抄袭

 

【访问限定符说明】

1.public修饰的成员在外部可以直接访问
2.proted和privat修饰的成员在外部不能直接访问
3.访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
4. 如果后面没有访问限定符,作用域就到 } 即类结束。
5. class的默认访问权限为private,struct为public(因为struct要兼容C)

注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别

【面试题】
问题:C++中的struct和class的区别是什么?


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

4.2封装

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

在类和对象阶段,主要研究的是类的封装特性,那什么是封装呢?

封装:将数据和操作数据的方法有机结合起来,隐藏对象的属性和操作细节,仅对外公布接口来和对象进行交互。

对于计算机使用者而言,不用关心内部核心部件,比如主板上线路是如何布局的,CPU内部是如何设计的等,用户只需要知道,怎么开机、怎么通过键盘和鼠标与计算机进行交互即可。因此计算机厂商在出厂时,在外部套上壳子,将内部实现细节隐藏起来,仅仅对外提供开关机、鼠标以及键盘插孔等,让用户可以与计算机进行交互即可。


5.类的作用域 

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

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

6.类的实例化 

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

1. 类是对对象进行描述的,是一个像模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它;比如:入学时填写的学生信息表,表格就可以看成是一个类,来描述具体学生信息。

2. 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量

int main ()
{
        Person . _age = 100 ;   // 编译失败: error C2059: 语法错误 :“.”
        return 0 ;
}

Person类是没有空间的,只有Person类实例化出的对象才有具体的年龄

你可以理解为我们使用类定义了一个“人类模板”这个模板就像一张图纸,就只是一个没有生命没有实体的模板而已。然后我们需要通过这个模板“实例化”出真实的对象,而这些通过模板实例化出来的对象也是各不相同,比如:性别有男有女,身高有低有高...但是性别、身高等等这些属性都是“人类模板”都具备的属性。而我们要去定义要去访问这些人的具体信息,就需要先通过类(模板)实例化出真人(对象)


7.类对象模型 

7.1计算类对象的大小 

class A
{
public :
        void PrintA ()
        {
          cout << _a << endl ;
        }
private :
        char _a ;
};

问题:类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算一个类的大小? 

7.2类对象的存储方法 

上图就是类对象的存储方法。类成员变量的属性,每个对象都有,而类中的方法,各个对象不一定都需要有,可能没用也可能有用。所以为了节省空间,在存储时只将类变量的属性存放在类中,这样每个实例化对象都具备这些属性,而将类中的方法放在公共代码区,这样就防止一些对象不需要那些方法但是实例化时却浪费了空间,而若有的对象需要执行方法时就去公共代码区找。

举个例子:现在有一个小区,小区的业主肯定每人都有一套房,而我们在建小区建房子时,每套房子肯定都要有客厅、卧室、厕所...这些是每个业主都需要有的,而关于健身锻炼,不一定每个业主都喜欢,那假如你在每套房子都建一个健身房是不是就可能浪费了?但是每个业主都有健身的权力,所以我们可以在小区的公共活动区域建一个锻炼区,想锻炼的业主就自己去公共区域的锻炼区锻炼即可。

小试牛刀:

// 类中既有成员变量,又有成员函数
class A1 {
public :
    void f1 ( )  {   }
private :
    int _a ;
};
// 类中仅有成员函数
class A2 {
public :
  void f2 ( ) {   }
};
// 类中什么都没有 --- 空类
class A3
{
};

sizeof(A1) : ____ __ sizeof(A2) : ____ __ sizeof(A3) : ____ __

结论:一个类的大小,实际就是该类中“成员变量”之和,当然要注意内存对齐
注意空类的大小,空类比较特殊,编译器给空类一个字节来唯一标识这个类的对象

7.3结构体内存对齐规则 

1. 第一个成员在与结构体偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的对齐数为8
3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

【面试题】 

1. 结构体怎么对齐? 为什么要进行内存对齐?
2. 如何让结构体按照指定的对齐参数进行对齐?能否按照3、4、5即任意字节对齐?
3. 什么是大小端?如何测试某台机器是大端还是小端,有没有遇到过要考虑大小端的场景


8.this指针  

8.1this指针的引用 

我们先来定义一个日期类Date 

class Date
{
public :
        void Init ( int year , int month , int day )
        {
                _year = year ;
                _month = month ;
                _day = day ;
        }
        void Print ()
        {
                cout << _year << "-" << _month << "-" << _day << endl ;

        }

       
private :
        int _year ;     //
        int _month ;     //
        int _day ;       //
};
int main ()
{
        Date d1 , d2 ;
        d1 . Init ( 2022 , 1 , 11 );
        d2 . Init ( 2022 , 1 , 12 );
        d1 . Print ();
        d2 . Print ();
        return 0 ;
}

对于上述类,有这样的一个问题:

Date类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调用 Init 函数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢

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

8.2this指针的特性 

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

我们平时见到的代码是左边这个,但是实际上底层逻辑是右边这个,只是因为我们的C++教父在发明C++时把这个指针调用的任务交给我们的编译器了,让编译器在每次都自动完成隐式调用。

【面试题】

// 1.this指针存在哪里?

一般是存在栈帧里面,this指针是函数的形参,那么一般函数的形参存在哪this指针就存在哪。当函数调用时,进行压栈存储。而vs这个编译器做了优化,使用ecx寄存器传递,因为成员函数中可能会频繁调用this指针,所以使用ecx寄存器效率会更快。

// 2. 下面程序编译运行结果是? A 、编译报错 B 、运行崩溃 C 、正常运行
class A
{
public :
        void Print ( )
        {
                cout << "Print()" << endl ;
        }
private :
        int _a ;
};
int main ()
{
        A * p = nullptr ;
        p -> Print ( );
        return 0 ;
}

答案是C

首先有的兄弟可能代码都不是很理解,怎么就搞了个指针调用??

其实是因为出题人想达到一个:空指针调用成员函数 的目的,所以才需要使用指针。可以参考上图,我们平时调用成员函数就是先实例化出一个对象然后调用,比如:d1.Print()但是实际上这仍是一个传址调用(参考上图)括号中有this指针在隐式调用

那么出题人想达到空指针调用函数的目的,肯定没办法像平常那样子,因为你没办法控制一个对象的地址是空呀,所以只能使用指针。所以题中的

  p->Print( );可以理解成Print(p);

而我们知道,成员函数的地址不在对象中,在公共代码区域,所以我们调用成员函数之后是不需要对对象进行解引用去访问什么的,所以即使调用的对象是空指针,我们不需要去解引用,也就不会报错,然后成员函数也只是简单地打印字符串,所以此题答案是C

// 3. 下面程序编译运行结果是? A 、编译报错 B 、运行崩溃 C 、正常运行
class A
{
public :
            void PrintA ()
          {
                cout << _a << endl ;
          }
private :
        int _a ;
};
int main ()
{
            A * p = nullptr ;
            p -> PrintA ();
            return 0 ;
}

此题答案B

这道题的调用道理同上,但是这道题的成员函数是需要访问对象的成员变量,也即是需要对调用者(对象)进行解引用访问,但是我们函数的调用者是空指针呀,所以就会发生对空指针解引用的问题,于是程序运行奔溃


9.类的6个默认成员函数

如果一个类中什么成员都没有,简称为空类
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

class Date { };


10.构造函数

10.1概念 

对于以下Date类

class Date
{
public :
        void Init ( int year , int month , int day )
        {
                _year = year ;
                _month = month ;
                _day = day ;
        }
      
        void Print ()
        {
                cout << _year << "-" << _month << "-" << _day << endl ;
        }
private :
        int _year ;
        int _month ;
        int _day ;
};
int main ()
{
        Date d1 ;
        d1 . Init ( 2022 , 7 , 5 );
        d1 . Print ();
        
        Date d2 ;
        d2 . Init ( 2022 , 7 , 6 );
        d2 . Print ();
        return 0 ;
}

对于Date类,可以通过 Init 公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?

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

10.2特性

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开辟空间创建对象,而是初始化对象

其特征如下:
1.函数名与类名相同
2.无返回值
3.对象实例化时编译器自动调用对应的构造函数
4.构造函数可以重载 

  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 ); // 调用带参的构造函数
      // 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
}  

5.如果类中没有显式定义构造函数,则编译器会自动生成一个无参的默认构造函数,一旦用户显式定义了则编译器不再生成

  class Date
{
  public :
        /*
        // 如果用户显式定义了构造函数,编译器将不再生成
        Date(int year, int month, int day)
        {
                _year = year;
                _month = month;
                _day = day;
        }
        */
        void Print ()
        {
                cout << _year << "-" << _month << "-" << _day << endl ;
        }
  private :
        int _year ;
        int _month ;
        int _day ;
};
  int main ()
{
        Date d1 ;
        return 0 ;
}

将Date类中构造函数屏蔽后,代码可以通过编译,因为编译器自动生成了一个无参的默认构造函数

将Date类中的构造函数放开代码则编译失败,因为一旦类中有显式定义了任何构造函数,编译器将不会再自动生成无参默认构造函数。而我们在main函数中又无参地实例化对象,所以会报错:error C2512: “Date”:没有合适的默认构造函数可用

6.C++把类型分为内置类型(基本类型)和自定义类型。内置类型就是语言提供的基本数据类型:int、char、double...自定义类型就是我们自己定义的如cass、struct、union。而C++在设计时这里埋了一个坑(不足之处,在后面有补丁解决这个缺陷)就是当我们没有自己显式定义构造函数而是让编译器自己生成无参默认构造函数并调用时,系统是给基本数据类型赋随机值,而对自定义类型则会去调用自定义成员的默认构造函数。

class Time
{
public:
	Time()
	{
		cout << "Time()默认构造函数" << endl;
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
	void Print()
	{
		cout << "Time:" << _hour <<"-"<< _minute <<"-"<< _second << endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
public:
	void Print1()
	{
		cout << "Date:" << _year << "-" << _month << "-" << _day << endl;
	}
	void Print2()
	{
		_t.Print();
	}
private:
	// 基本类型(内置类型)
	int _year;
	int _month;
	int _day;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d;
	d.Print1();
	d.Print2();
	return 0;
}

如运行结果所示,从结果我们便能感受到构造函数的第6个特性了吧         

于是在C++11中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。

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 = 1970 ;
        int _month = 1 ;
        int _day = 1 ;
      
        // 自定义类型
        Time _t ;
};
int main ()
{
        Date d ;
        return 0 ;
}

7.无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都能认为是默认构造函数。

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 ;
};
// 以下测试函数能通过编译吗?
void Test ()
{
        Date d1 ;
}

如上的test函数中的实例化对象是无法通过编译的。虽说构造函数支持重载,在类中两个构造函数的确也是实现了重载,但是这样会出现二义性,编译器将无法判断你要调用哪一个。 


11.析构函数

11.1概念

通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没的呢?

析构函数:与构造函数功能相反,析构函数不是完成对象本身的销毁,局部对象销毁工作是由编译器完成的,一旦函数调用结束,栈空间就会被回收。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。

11.2特性

析构函数是特殊的成员函数,其特性如下:

1.析构函数是在类名前加上字符~
2.无参数无返回值类型
3.一个类只能有一个析构函数。若为显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
4.对象生命周期结束后,C++编译器自动调用析构函数
5.编译器生成的默认析构函数,对自定义类型成员调用它的析构函数
6.如果类中没有申请资源,析构函数不用写,直接使用编译器默认生成的析构函数,比如Date类;有资源申请时一定要写,否则会导致资源泄露,比如Stack类。

  • 9
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 16
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

KGoDY

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值