类和对象

类和对象


面向过程和面向对象

面向过程

  • 函数堆积(在乎的是过程)

面向对象

  • 对象之间的交互(在乎的是模块)

面向过程和面向对象的关系()

  • 例如:蛋炒饭于盖浇饭的区别

类的定义

Class 类名
{
    函数;
    变量
};
  1. 类里面的函数可以在类里面进行定义
    1. 缺点:多次引用的时候会出现函数重复定义
    2. 有点:但是编译器很有可能会将它定义为内联函数
  2. 类的声明(函数的声明)在.h 头文件里,类的定义(函数的定义)在.c 文件里

类的封装定义

类+访问限定符 就叫封装

  • 访问限定符:
    1. public
    2. private 在类外不能被一个对象直接访问
    3. proteced 在类外不能被一个对象直接访问

不加访问限定符的class类成员默认都是private的。
不加访问限定符的struct成员默认都是public的,为了和c语言的struct的性质兼容。
通过指针的方式可以修改任何限定符的限定的成员。

注意:访问限定符只在编译的时候有用,但是在运行时时不起作用的(最常用的体现方式就是我们通过public的函数来修改private的变量成员)

编译器如何编译一个类

  1. 识别类名
  2. 识别类中成员变量
  3. 识别类中的成员函数并对成员函数进行修改
    1. 每个的函数加上一个this指针形参
    2. 在调用的时候编译器会将此形参自动传参,但大部分的this指针式通过寄存器传参的,而可变参数函数是通过压栈的方式传参的。

this指针

  • 指向当前对象,在当前类函数中使用
  • 当前对象中并不包含this指针
  • this类型:类类型* const,所以不能人为的置空
  • this是成员函数第一个默认参数,由编译器通过ecx(thiscall)寄存器来自动传递
  • 不定参数函数的this参数是通过压栈传递的(_cdecl)

有如下坑人的代码:

class Test
{
public:
    void TestF()
    {
        this->a = 0;
    }
private:
    int a;
};

int main()
{
    Test *t = NULL;
    t->TestF();
}
//问:此代码为什么会崩溃?

你可能认为是因为t 指针是空的,所以访问其成员函数会出错。
这样想的话,你就大错特错了,首先在调用TestF函数的时候是通过这样的方式调用的:

Test::TestF(t);

而根本就不会去解引用t指针,所以在调用的时候是不会出错的,出错的地方在调用函数之后,对形参this进行解引用,此时的this就是实参t,所以在函数内部解引用一个空指针会出错。

类的大小的计算

类成员变量相加(注意内存对齐)
类成员在内存中的存储:

  • 成员1
  • 成员2

可见类中成员在内存中的存储并没有存储类的成员函数。

一种初始化方式

int main()
{
    int a=0;
    int b(0);//与int b = 0是一样的效果
}

这种初始化的方式也可以对对象初始化,调用的是拷贝构造函数。

默认的六个成员函数

构造函数

  1. 创建一个对象的时候编译器会自动执行的一个函数(此函数执行完成之后,此对象才算创建完毕)(thiscall调用约定)
  2. 构造函数的函数名和类名相同,并且不能设置返回值
  3. 构造函数在整个对象的生命周期只能被调用一次
  4. 构造函数可以在类中定义,也可以在类外定义
  5. 构造函数可以重载
  6. 定义类的时候,编译器会给它合成一个缺省的构造函数(说明每个类都有构造函数),并且此构造函数没有参数,也什么都不干。如果用户定义了构造函数,编译器则不会生成缺省的构造函数
  7. 全缺省参数的构造函数和无参的构造函数都叫做缺省构造函数,并且这种构造函数只能存在一个
  8. 构造函数的函数体里面是赋值,不是初始化。赋值可以是多次,但初始化只能是一次
  9. 构造函数的初始化列表(完成各个变量的初始化,如果不加,编译器会加上默认的参数初始化列表):

    class Test
    {
    public:
        Test(int arg1, int arg2)
            : _arg2(arg2)//注意是冒号
            , _arg1(arg1)
    private:
        int _arg1;
        int _arg2;
    }
    
    1. 注意:初始化列表中各个成员变量的出现按先后次序无关,按照成员在类中的声明次序进行初始化
    2. 建议:初始化列表中的而成员次序与成员在类中的声明 次序保持一致
    3. 用以下方式进行初始化:

      Test t1(1,2);
      //会将1先赋给arg1,将赋给arg2,然后在类中成员变量中找出声明的顺序,
      //在通过初始化列表找到要初始化的值,也就是先将arg1赋给成员变量_arg1,
      //然后将arg2赋给成员变量_arg2
      

在初始化列表中使用this指针会失败的原因:因为还没有初始化完成,所以这个对象还不完整,所以不能使用this指针

那些成员必须在初始化列表中进行初始化?

  • 如果类的成员中有引用,那么此类在创建对象的时候,只能用构造函数的初始化列表进行初始化。
  • 如果类中有被const修饰的成员变量
  • 如果类的成员变量中有另一个类的对象(该类中的构造函数含有非缺省的参数)。

析构函数

  1. 销毁一个对象(即对象的生命周期到了)的时候编译器会自动执行一个函数(thiscall调用约定)
  2. 析构函数的函数名是在类名的前面加上一个~ 符号,并且既不能设置参数也不能设置返回值
  3. 析构函数在整个对象的生命周期只能被调用一次
  4. 析构函数可以在类中定义,也可以在类外定义
  5. 析构函数不可以重载,因为析构函数没有参数
  6. 定义类的时候,编译器会给它合成一个缺省的析构函数(说明每个类都有析构函数),并且此析构函数没有参数,也什么都不干。如果用户定义了析构函数,编译器则不会生成缺省的析构函数
  7. 析构函数的调用顺序和对象创建的顺序相反(因为是对象是在栈里面,所以需要符合栈的特性)

拷贝构造函数

  1. 单参数,并且是类类型的引用(原因如下:)
    1. 如果不传引用可以通过编译,那么传的引用为传值
    2. 传值的时候因为实参是一个对象类型,所以,先得生成实参对象,而生成实参对象的方式就是通过拷贝构造函数。
    3. 而拷贝构造函数是通过传值的,所以还需要创建形参对象。。。。形成无休止的递归。
    4. 因此拷贝构造函数只能传类类型的引用。
  2. 创建对象时,使用同类对象来进行初始化
  3. 拷贝构造函数是拷贝类中的所有成员变量,包括栈中数组的每个元素,原封不动的拷贝
    • 如果类中有指向堆里面的指针,在拷贝之后两个对象将会指向同一块堆空间,那么在销毁的时候就可能会出现两次free同一块空间。

运算符重载

将一个对象之家二赋给相同类的另一个对象是可以的(直接用等号相连),其默认的作用和默认的拷贝构造函数一样,所以也有可能造成多次释放堆空间,并且赋值不会释放原有的空间,造成内存泄露。

  • 运算符重载时操作数的个数必须和此运算符的操作数的数量相同(注意重载时候编译器加入的this指针,即this为双操作树运算符的左操作数)
  • 运算符也可以重载在普通函数的位置,但是不会有this指针,所以应注意操作数的个数,并且操作数至少有一个类类型的对象。
  • 重载运算符不会改变运算符的优先级,和运算方向。
  • 运算符重载时,运算前后值不需要变化的操作数最好加上const,并且为了更小的开销,传参的时候最好传引用,返回类类型变量的时候最好也是返回引用。
  • 注意两种++运算符的重载。

    //前置++
    Date& operator++()
    {
        this->_day += 1;
        return *this;
    }
    //后置++
    Date operator++(int)
    {
        Date tmp(*this);
        this->_day += 1;
        return tmp;
    }
    
  • 输出运算符<< 的重载

    在类中建立友元关系:
    friend ostream& operator<<(ostream& _cout, const Date& d);
    ostream& operator<<(ostream& _cout, const Date& d)
    {
    _cout<< d._year << “-” << d.mouth << “-” << d.day;
    return _cout;
    }

普通类型的取地址符的重载

const operator&()
{
    return this;
}

const修饰的取地址符的重载

const operator&()const
{
    return this;
}

友元函数

在类中声明 friend [函数声明];
在友元函数中可以访问此类中的私有成员。
一个函数可以式多个类的友元函数

友元类

在类中声明 friend [class 类名]
在友元类中可以访问声明所在类中的私有成员
友元关系不可传递
友元关系是单向的

友元的优点

  • 减少了开销

友元的缺点

  • 破坏了类的封装特性

const关键字

const修饰普通类型的变量使变量具有常量属性,并且是在编译的时候进行替换
const修饰类的成员函数,在此函数中不可以修改类的成员变量

  • 成员函数+const = 成员函数的this指针前面加上const

    class Test
    {
    public:
        void TestConst()const
        {
            ...
        }
    private:
        ...
    }
    

以上的成员函数在编译器编译后加上this指针后会变成如下

void TestConst(Test *cosnt this)const

因为成员函数加上const修饰代表其函数内不能修改类成员变量的值,所以就可以用一下方式替代

void TestConst(const Test *const this);

cosnt修饰的对象只能调用const修饰的类的成员函数。


  • 因为const修饰的对象意思就是此对象的成员变量不可修改,但是如果调用非const修饰的成员函数,而此成员函数就有可能修改对象的成员变量,所以为了安全,const修饰的对象不能调用const修饰的类成员函数。
  • cosnt修饰的成员函数本来就是不让改变成员变量,所以const修饰的对象可以调用const修饰的成员函数。

非const修饰的对象可以调用cosnt修饰的成员函数
如果想要指定的const成员函数修改某个成员变量,可以跟此成语变量前面加上mutable 关键字(这样使const修饰的对象变得又不安全了)
函数返回指针的时候,如果不希望此指针指向的内容被修改,那么就需要返回的指针类型为const 指针。
返回被const修饰的形参的时候,返回值的类型必须被const修饰
构造函数不可以使用const修饰
友元函数不能用const修饰
explicit修饰构造函数会抑制构造函数的类型隐式转换。
编译器感觉自己需要的时候才会合成默认的构造函数(和前面讲的有点矛盾)

static修饰类成员

static修饰的成员变量叫做静态成员,静态成员为此类的所有对象共享。
静态成员需要在声明的时候加上static ,定义的时候不需要加,如果要成员函数的定义在类里面的时候直接讲static加在函数之前即可。
静态成员变量在类外可以通过 类名::成员变量 来访问

static修饰类成员变量(静态成员变量):
  1. 在类中使用static 类型 变量名 来声明静态成员变量,可以在三个访问限定符的任何一个之中,也可以使用const 来修饰。
  2. 静态成员必须在类外定义并初始化,并且不需要加static类型 类名 变量名=初始化值 来进行初始化。
  3. 类的大小不包含静态成员变量。
  4. 静态成员变量不能房子初始化列表总进程初始化,因为初始化列表是初始化类的成员变量。
static修饰成员函数(静态成员函数):
  1. 在类中直接在函数前面加上static即代表此函数是静态成员函数。
  2. 静态的成员函数不能访问非静态的成员变量。
  3. 静态的成员函数只能调用静态的成员变量和静态的成员函数。
  4. 非静态的成员函数可以调用静态的成员函数和静态成员变量。
  5. 静态成员函数不能访问非静态的成员函数和变量,也不能用const修饰,因为静态的成员函数没有编译器自动传递的 this 指针。

类的设计

  1. 根据问题的解决方法设计数据模型。
  2. 根据对数据模型的抽象(包含什么样的数据,以及方法)
  3. 对类进行实例化(创建对象)。
  4. 实现封装:
    1. 对类的细节进行封装(打包成整体)
    2. 对象与对象之间进行交互(将相应成员进行设置)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值