C++:类和对象

什么是类

  • 脱胎于C的struct,但在C++中,struct不但可以定义数据以描述类的属性/成员变量,还可以再类里定义函数,用以描述类的方法(成员函数),常使用使用class替换作为关键字,但两者之间略有区别
  • 定义一个典型的类:
  1. 声明/定义都放在类体中-编译器可能会将成员函数当作内联函数处理
class Person{
   public: 
       void showInfo(){
          cout<<name<<"-"<<age<<"-"<<endl;
       }
   private:
      char* name;
      int age;      
};
  1. 声明放在.h文件中/定义放在.cpp中-规避了第一种所述的风险
//.h文件
class Person{
   public: 
       void showInfo();
   private:
      char* name;
      int age;      
};
//实现文件.cpp
#include "Person.h"

void Person::showInfo(){
    cout<name<<"-"<<age<<"-"<<endl;
}

封装/访问限定符

  • 封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节, 将一个对象的属性与行为结合在一起,使其更符合人们对于一件事物的认知,将属于该对象的所有东西打包在一起;通过访问限定符选择性的将其部分功能开放出来与其他对象进行交互,而对于对象内部的一些实现细节,外部用户不需要知道,用就完事了;
  • 通过访问限定符来实现:
    • public-公有:修饰的成员在类外可以直接被访问;
    • protected-保护&private-私有:在类外无法被直接访问;
    • class默认访问权限为private,struct为public(兼容C);
    • 访问限定符的作用域是从该访问限定符出现的位置开始直到下一个访问限定符出现时为止;
    • 访问限定符值在编译时有用,当数据被映射到内存后,没有任何访问限定符区别;
如何在类外访问一个私有的成员变量?
  • 在类里再设定一个共有域,调用私有成员变量,这样就可以通过访问这个公有的成员来访问私有成员0变量;
class与struct的区别是什么?
  • 当未访问限定符时:struct默认公有/class默认私有;

类的实例化

  • 即定义了一个类的变量,通常叫做类的对象;创建这个对象的过程,就叫做实例化;
  • 计算类对象的大小:
    • 一个类的大小,实际上就是该类中的“成员变量”之和,当然也要进行内存对齐(VS默认8bit,Linux下的gcc没有固定的默认字节序),注意空类的大小,空类比较特殊,编译器给了空类一个字节在唯一标识这个类
    • 当类里面嵌套了类时,如果该嵌套类没定义对象,该类是不计入大小的,只是一个声明;
C++内存管理:
区域名称作用
系统数据区操作系统运行所必须的内存
存放局部变量和形参
自由存储区malloc动态申请的内存块
new申请的内存块
全局/静态存储区全局变量/静态变量(不再有bss段和data段的区分)
常量存储区(const区)存放的是常量,不允许修改
代码段存放运行程序代码
为什莫要进行内存对齐?
  • 提高效率,在进行内存对齐后,cpu访问内存效率大大提升,因为在CPU看来,内存是一块一块的,所以它取数据的时候也会一块一块的取,这个块大小的叫做内存读取粒度(memory access granularity),假设cpu的内存读取粒度是4,也就是说我cpu一次拿四个字节的数据,

    • 当不进行内存对齐时:
      在这里插入图片描述

    可以看到,当一次拿到4个字节时,我不但拿到了char型数据,还有之后int型数据的前三个字节,当我要拿到int型的那个完整数据时,还要把之前的数据在拼接一次,这样就等于走了3步(宏观上看);

    • 进行内存对齐时:
      在这里插入图片描述

    而内存对齐后,我第一次拿到char型数据,第二次拿到int型数据,完全不用拼接, 这样就提高了cpu读取数据的效率

    • 平台移植性:因为有的硬件平台只允许在一些地址处取特定类型的数据(比如一次取四个字节,或两个),否则就会异常甚至宕机;
  • 修改默认对齐数的方法:

#pragma pack(4);

类的6个默认成员函数

  • 在一个空类中,会自动生成6个默认成员函数
class Date {};

分类:

  • 初始化和清理
    • 构造函数主要完成初始化工作
    • 析构函数主要完成清理工作
  • 拷贝/复制
    • 拷贝构造是使用同类对象初始化创建对象
    • 赋值重载主要是把一个对象赋值给另一个对象
  • 取地址重载
    • 主要是普通对象和const对象取地址,这两个很少会自己实现;

构造函数

  1. 构造函数指一种特殊的成员函数,名字与类名相同,类创建对象后会自动调用,目的是对成员变量进行初始化;
  2. 特征:
    • 函数名与类名相同;
    • 无返回值;
    • 对象实例化时编译器自动调用对应的构造函数;
    • 构造函数可被重载
    • 分为带参/无参;
class Date{
    public:
       void Date(int year,int month,int day)
       :_year(year)//括号内放初始值或表达式;
       ,_month(month)
       ,_day(day)//构造函数体初始化列表只可进行一次;
       {
         _year = year;//构造函数体赋值,在一定场景下,可以起到初始化作用,但不是真正的初始化,可多次
         _month = month;
         _day  = day;
       }
    private:
    int _year;
    int _month;
    int _day;
};
int main(){ 
   Date d1(2019,3);//有参调用法;
   Date d3;//无参构造调用法;
   return 0;
}
  • 如果class里有引用成员变量/const成员变量/类类型成员(该类无默认构造函数-无参/全缺省/自动生成)时,必须使用初始化列表初始化!
    • 这是因为引用成员引用定义的时候必须初始化,只有在对象实例化引用才会定义,而在对象实例化定义就是在构造函数的初始化列表里。
    • const成员变量在定义时也必须初始化,所以构造函数要使用初始化列表初始化
  • 即使没有上述的成员,尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化
  • 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关:
 class Array
{
public:
    Array(int size)
      :_size(size)
      , _array(_size/2)
    { }
private:
    int* _array;
    int _size;
};
  • 当构建完后我们会发现_array是一个随机值,因为编译器是按照定义顺序先初始化_array,此时_size还未初始化,还是个随机值;
explicit关键字
  • 构造函数不仅可以构造与初始化对象,对于单个参数的构造函数,还具有类型转换的作用
  • 用explicit修饰构造函数,将会禁止单参构造函数的隐式转换
class Date
{
 public:
       Date(int year)
             :_year(year)
       {}
      explicit Date(int year)
             :_year(year)
       {}
 private:
       int _year;
       int _month:
       int _day;
};
void TestDate()
{
Date d1(2018);
// 用一个整形变量给日期类型对象赋值
// 实际编译器背后会用2019构造一个无名对象,最后用无名对象给d1对象进行赋值;
d1 = 2019;//但这样有时会给你一种d1是int型的错觉,我们完全可以使用explicit关键字来禁止这种方法的书写;
}

析构函数

  • 与构造函数功能相反,不完成对对象销毁,而是再对象被编译器销毁时完成类的一些资源清理工作(空间申请之类的)
  • 特性
    • 析构函数名是在类名字前~-;
    • 无参无返回;
    • 有且只有一个;
    • 对象生命周期结束时,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;
};

拷贝构造函数

  • 只有单个形参,该形参是对本类类型对象的引用(const修饰),用已存在的雷类型对象创建新对象时自动调用;
  • 拷贝构造函数是构造函数的一个重载形式
  • 拷贝构造函数的参数只有一个且必须使用引用传参,因为使用传值方式会引发无穷递归调用:
    在这里插入图片描述
    这是因为每次给拷贝构造函数传参一但是传值,就要创建临时的d1,那就要再次调用拷贝构造,如此循环往复无法停止,而利用引用的特性就不会创建临时拷贝,所以只在d2创建时使用一次拷贝构造函数即可;
  • 若未显式定义,系统默认生成拷贝构造函数,但默认的拷贝构造函数对象按内存存储按字节序完成拷贝(拷贝指针也是把地址考过来,指向的依然是同一块空间),是值/浅拷贝
    一般形式:
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;
      }
  private:
     int _year;
     int _month;
     int _day;
};

int main(){
    Date d1;
    Date d2(d1);
    return 0;
}

运算符重载

  • 运算符重载就相当于为这个运算符在系统规定的运算情况外自定义规则(结构体,类参与的运算),我把它理解为另类的函数,操作符就是函数名;
  • 这是一种针对自定义类型对原有操作符功能的扩展;
  • 增强代码可读性
    • 函数名字:关键字operator后面接需要重载的运算符符号
    • 函数原型:返回值类型operator操作符(参数类型)
  • 注意:
    • 重载操作符必须有一个类类型或者枚举类型操作数(这样你才能跳出这个操作符被系统定义的普通类型运算的范围之外,否则就让系统不知道该采用哪个)
    • .*-::-sizeof-?:-.这五个操作符不可被重载;
    • 作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的操作符有一个默认的形参this,限定为第一个形参;
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;
      }
     bool operator==(const Date& d2)//bool operator(Date* this,Date& d2)
     {
         return this->_year==d2._year && this->_month == d2._month;
     }
     int _year;
     int _month;
     int _day;
};

int operator+(Date& d1,int b){
     return d1._month - b;
}

bool operator==(Date& d1,Date& d2){
     return d1._year == d2._year && d1._month == d2._month;
}

int main(){
    Date d1(2018,9);
    Date d2(2018,9);
    int d3 = d1+b;
    cout<<(d1 == d2)<<endl;
    cout << d1.operator==(d2)<<endl;
    return 0;
}
赋值运算符重载
 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 ;
};
int main(){
     Date d1(2019,3);
     Date d2(2018,3);
     //d1的赋值给d2;
     d2 = d1;
     //拷贝构造一个新对象d3
     Date d3 = d1;
     d2 = d1 = d3;//赋值运算符重载有了返回值就行了;d1.operator =(d3)-->d2.operator(d1)
}

赋值运算符主要有4点:

  1. 参数类型
  2. 返回值
  3. 检测是否自己给自己赋值
  4. 返回\this
  5. 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝。

static成员

  • 又称为类的静态成员,static修饰成员变量称为静态成员变量,修饰成员函数称为静态成员函数;
  • 类内定义,类外初始化(不可在构造函数初始化)-类型+作用域+成员名 = 初始化值;
  • 相同类的所有对象共同拥有,均可读写,不属于某个具体实例;
  • 类静态成员可以使用类名::静态成员/对象.静态成员来访问;
  • 静态成员函数没有隐藏this指针,不能访问非静态成员;
  • 静态成员也有访问级别,也有具体返回值,const修饰等;
  • 分为带参/无参;
class Date{
    public:
       void Date(int year,int month,int day)
       :_year(year)
       ,_month(month)
       ,_day(day)//构造函数体初始化列表;
       { }
       Date(const Date& d){
          _count++;
          _year = d_year;
          _month = d_month;
          _day = d_day;
      }
    private:
    int _year;
    int _month;
    int _day;
    static int _count;//定义
};
//static成员初始化
int Date::_count =0;
int main(){ 
   Date d1(2019,3,27);
   cout<<Date::_count<<endl;//两种调用方式;
   cout<<d1._count<<endl;
   return 0;
}
静态成员函数可以调用非静态成员函数吗?
  • 非静态成员函数需要this指针作为默认参数,而静态成员函数没有this指针,也无法拿取,所以无法调用;
  • 非静态成员函数可以调用静态成员函数。
  • C++11支持非静态成员变量在声明时,直接初始化,对于无参构造相当于给一个缺省值。不用构造函数初始化列表了

友元函数/友元类

  • 友元提供了一种突破封装的方式,有时提供了便利,但友元会增加耦合度,破坏了封装,所以友元不宜多用
友元函数
  • 问题:现在我们尝试去重载operator<<,然后发现我们没办法将operator<<重载成成员函数。因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以我们要将operator<<重载成全局函数。但是这
    样的话,又会导致类外没办法访问成员,那么这里就需要友元来解决。operator>>同理;
class Date
{
friend ostream& operator<<(ostream& outs,Date& d);
friend istream& operator>>(istream& ins,Date& d);
public:
     Date(int year,int month.int day)
            : _year(year)
            , _month(month)
            , _day(day)
         {}
     /*ostream& operator<<(ostream& _cout)
     { 
         _cout<<d._year<<"-"<<d.month<<"-"<<d._day;
         return _cout;
     }*/
private:
    int _year;
    int _month;
    int _day;
};
ostream& operator<<(ostream& outs,Date& d){
     outs<<d._year<<"-"<<d.month<<"-"<<d._day;
     return outs;
}
istream& operator>>(istream& ins,Date& d){
     ins>>d._year;
     ins>>d.month;
     ins>>d._day;
     return ins;
}
int main()
{ 
   Date d(2017.12.5);
  /* d1.operator<<(cout);
   d<<cout;//第一个参数是this指针*/
   operator<<(cout,d1);
   cin>>d1;
   cout<<d1;//此时书写顺序正常,但不使用友元函数,就必须把成员变量变成公有暴露出来,才能被外部函数operator<<访问;
   return 0;
}

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

友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。

  • 友元关系是单向的,不具有交换性;
  • 友元关系不能传递; 如果B是A的友元,C是B的友元,则不能说明C是A的友元。
friend class class_name;

内部类

概念及特性
  • 概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。注意此时这个内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去调用内部类。外部类对内部类没有任何优越的访问权限。
  • 注意:内部类就是外部类的友元类。注意友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元
  • 内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名。
  • 计算外部类大小时,不计算内部类,除非内部类在外部类里定义了对象;
class A{
   class B{
       void display(A& a){
           cout<<a._a<<endl;
           cout<<_s<<endl;
           cout<<a._S<<endl;
       }
   };
   int _a = 10;
   static int _s;
};
int A::_s = 5;

练习

看到这里,相信你已经对类有了一个基本的了解,不妨参考构建一个简单的Date类来牛刀小试一番吧!

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值