【C++】类和对象知识点总结

目录

一、类和对象入门

(一)引入类与对象

(二)成员函数的定义

(三)类对象的大小

(四)隐含的this指针

二、类的六个默认成员函数

(一)构造函数

1.带参构造函数

2.自己实现的无参构造函数

3.自己实现的全缺省构造函数

4.编译器自动生成的构造函数

5.注意

(二)析构函数

1.自己写的析构函数

2.编译器自动生成的析构函数

(三)拷贝构造函数

1.自己写的拷贝构造函数

2.编译器自动生成的拷贝构造函数

(四)赋值运算符重载

1.运算符重载

(1)==的重载

(2)>的重载

2.赋值运算符重载

(1)自己写的赋值运算符重载

(2)编译器自动生成的赋值运算符重载

(3)对比拷贝构造和赋值运算符重载

(五)取地址运算符重载

(六)const取地址运算符重载

三、关于类中的一些细节

(一)关于构造函数的一些细节

1.初始化列表

2.explicit关键字

(二)友元

1.友元函数

2.友元类

(三)const成员

1.const修饰引用(常引用问题)

2.const修饰类的 成员函数

(四)static成员

1.static成员变量

2.static成员函数

四、理解封装


一、类和对象入门

面向对象更关注对象与对象之间的关系;面向对象的三大特性:封装、继承、多态。

(一)引入类与对象

在C语言中,结构体中只能定义变量

在C++中,类(class)和结构体(struct)中可以定义两种:1.成员变量 2.成员函数

注意:一般情况下成员变量都是比较隐私的,都会定义成私有(private)或保护(protected)

私有和保护就像一把锁:他不让门外面的人进来,只可以是门里面的人进行访问

既然C++中都可以用来定义类,那么类和结构体之间有什么区别呢?

(1)默认的访问限定符不同:class默认是私有的,struct默认是共有的。

(2)对于C++中的struct:他不仅可以用来定义类,他还兼容了C中struct定义结构体的用法。

(二)成员函数的定义

1.在类里面定义:编译器可能将成员函数当作内联函数

2.在类里面声明,类外面定义:需要使用(::)符号

(三)类对象的大小

如何计算一个类实例化出的对象的大小?计算成员变量之和,并且考虑内存对齐规则

问题一:为什么类对象只保存成员变量,而成员函数放在公共代码段?

答:一个类实例化出N个对象,每个对象的成员变量都可以存储不同的值,但是调用的函数却是

同一个。如果每个对象都放成员函数,但是这些函数却是一样的,那么就会存储重复的东西,浪

费空间。所以将成员函数存在公共代码段,类实例化出的对象的大小不包含成员函数,只包含成

员变量。

问题二:没有成员变量的类的大小是1 -> 为什么是1,而不是0?

答:开1个字节不是为了存数据,而是占位。

(四)隐含的this指针

this指针的本质:成员函数的形参(他默认就是成员函数的第一个形参)。

this指针指向谁?哪个类调用成员函数,this指针就指向调用他的类。

如果定义class A,那么一个指向A的指针所指向的空间只存着成员变量,没有存成员函数。

成员函数通过this->_var这种形式就可以访问到成员变量,只是这件事是编译器帮我们完成的。

二、类的六个默认成员函数

六个默认成员函数特点:自己没写编译器自动生成,自己写了编译器就不会生成了

负责初始化和清理的两个函数:构造函数(负责初始化)、析构函数(负责资源清理工作)

负责拷贝复制的两个函数:拷贝构造函数、赋值运算符重载

两个取地址操作符重载函数:取地址运算符重载,const取地址运算符重载

(一)构造函数

完成对象的初始化工作,而不是对象的创建。对象创建时自动调用完成初始化工作。

特征:函数名与类名相同、无返回值、支持重载(有以下四种形式)

下面通过日期类来说明这四种构造函数(关于日期类的定义请关注另一篇博客

1.带参构造函数

定义

    Date(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }

调用方式

Date d(2023,10,30);

2.自己实现的无参构造函数

定义

    Date()
    {
        _year = 0;
        _month = 1;
        _day = 1;
    }

调用方式

    Date d2;//调不带参数的构造函数,不加括号
    //Date d2();//不能加括号,要是调带参数的,那就要加括号了

3.自己实现的全缺省构造函数

使用全缺省的目的:将带参构造函数和无参构造函数合二为一。(最常用)

定义

    Date(int year = 0, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }

调用方式

Date d1(2023,11,14);//这种是初始化成自己设置的值
Date d2;//这种是初始化成默认值0,1,1

4.编译器自动生成的构造函数

如果我们没有显示定义(自己写)构造函数,那么C++编译器会自动生成一个无参的默认构造函数

对于 编译器自动生成的无参默认构造函数(双标)

(1)针对内置类型的成员变量 没有做处理

(2)针对自定义类型的成员变量,调用他的构造函数去初始化

一旦用户显示定义了,编译器将不再生成

5.注意

(1)默认构造函数有3种  特点:不用传参数

自己实现的无参的构造函数②、自己实现的全缺省构造函数③、编译器自动生成的构造函数④

(2)三种默认构造函数不能同时存在,他们三个只能同时存在一个。

(3)对比两个概念

默认成员函数:指的是最开始说的那六个,自己不写编译器自动生成。

默认构造函数:说的是(1)中的三种,不传参就能调用。

(4)如果采用声明和定义分离的形式,缺省参数在声明处给就好了,不用在定义处给了。

(5)C++11中支持一种全新的给出缺省参数的方式

class Date
{
public:
    Date(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }

//c++11支持的在声明时给缺省值(注意这不是初始化)
private:
    int _year = 0;
    int _month = 1;
    int _day = 1;
};

(二)析构函数

完成对象里面的资源清理工作,不是对象的销毁。 对象生命周期到了以后自动调用完成清理工作

特征:函数名(~类名)、无返回值无参数、不支持重载

问题:构造和析构的顺序?先构造的后析构。原因:因为对象是存在栈上的,满足后进先出。

Date d1;
Date d2;

先构造d1,后构造d2;先析构d2,后析构d1。

1.自己写的析构函数

以栈类为例:注意⚠️函数名(~类)

class Stack
{
public:
    //构造函数
    Stack(int n = 10)
    {
        _arr = (int*)malloc(sizeof(int)*n);
        _size = 0;
        _capacity = n;
    }
    
    //析构函数
    ~Stack()
    {
        free(_arr);
        _arr = nullptr;
        _size = _capacity = 0;
    }
    
private:
    int* _arr;
    size_t _size;
    size_t _capacity;
};

2.编译器自动生成的析构函数

如果我们没有显示定义(自己写)析构函数,那么C++编译器会自动生成一个析构函数

对于 编译器自动生成的析构函数(双标)

(1)针对内置类型的成员变量,没有做处理。

(2)针对自定义类型的成员变量,调用他的析构函数。

一旦用户显示定义了,编译器将不再生成

(三)拷贝构造函数

1.自己写的拷贝构造函数

首先说明:1.拷贝构造函数是构造函数的一个重载形式  2.拷贝构造函数只有一个引用参数

解释:为什么拷贝构造函数只有一个引用参数,而且传值方式会引发无穷递归?

Date d(d1);

将d1传给d是一次拷贝构造(函数传参),但这时拷贝构造函数还没有写呢。也就是说在拷贝构

造函数还不存在的情况下就要调用拷贝构造函数了,所以就会引发无穷递归。

拷贝构造函数的定义(以日期类为例)

解释const的作用:保护d为别名的那块空间的内容不被修改。

Date(const Date& d)
{
    _year = d._year;
    _month = d._month;
    _day = d._day;
}

拷贝构造函数的调用(两种方式)

Date d(d1);    //方式一
Date d = d2;   //方式二

2.编译器自动生成的拷贝构造函数

编译器生成的拷贝构造,会完成按字节的值拷贝(浅拷贝)。

对于Date类这种,完成浅拷贝即可,所以编译器生成的足够用。但是Stack类这种,必须自己去

实现深拷贝。

(四)赋值运算符重载

要了解赋值运算符重载,我们可以先学习运算符重载

1.运算符重载

对于内置类型(int long···)可以直接使用==  !=这种运算符。

对于自定义类型,我们不可以直接使用自带的运算符;要想直接使用,就要用到运算符重载。

有关运算符重载的几点说明

①对于运算符重载,有一个关键字operator

②.*    ::    sizeof    ?:    .    这五个运算符不能重载。

下面用日期类来演示具体用法。

(1)==的重载

第一种写法:将运算符重载函数写在类外面(这样写要将成员变量改成public)

注意⚠️运算符有几个操作数,operator重载的函数就有几个参数。

注意⚠️自定义类型是不能用运算符的,要用就得实现重载函数,自定义类型用的时候等价于调

用这个重载函数。

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

//使用
cout << (d1 == d2) << endl;

第二种写法:将运算符重载函数写在类里面(这样写要将成员变量可以是私有)

这种写法更常用,因为我们一般都需要成员变量是私有的。

//这里实际上有两个参数:一个是this指针,另一个是操作数
bool operator==(const Date& d)//bool operator==(Date* this,const Date& d)
{
    return _year == d._year && _month == d._month && _day == d._day;
}

//使用
cout << (d1 == d2) << endl;
(2)>的重载
bool operator>(const Date& d)
{
    //年大的日期一定大    
    if(_year > d._year)
    {
        return true;
    }
    //年一样,月大的一定大
    else if(_year == d._year && _month > d._month)
    {
        return true;
    }
    //年一样,月一样,日大的一定大
    else if(_year == d._year && _month == d._month && _day > d._day)
    {
        return true;
    }
    
    //其他情况都是false
    return false;
}

//使用
cout << (d1 > d2) << endl;

这里只介绍==和>的重载,因为这足以说明运算符重载的写法。

xxx以日期类为例,介绍了各种运算符重载。

2.赋值运算符重载

(1)自己写的赋值运算符重载
//这里返回值类型为Date是为了实现连续赋值
//用引用返回是因为返回值*this出了函数作用域还存在
//引用返回还可以减少拷贝构造,提高效率
Date& operator =(const Date& d)
{
    if(this != &d)//针对自己给自己赋值的检查判断
                  //如果出现自己给自己赋值,可以不走下面的步骤,提高效率
    {
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }
    return *this;
}
(2)编译器自动生成的赋值运算符重载

编译器生成的operator=,会完成按字节的值拷贝(浅拷贝)。

对于Date类这种,完成浅拷贝即可,所以编译器生成的足够用。但是Stack类这种,必须自己去

实现深拷贝。

(3)对比拷贝构造和赋值运算符重载

①调用形式上的差别

//拷贝构造
Date d(d1);
Date d = d1;

//赋值运算符重载
d2 = d1;

②拷贝构造函数和operator= 都是默认成员函数,我们不现实时,编译器会帮我们实现一份。

我们不现实时,编译器生成的拷贝构造和operator=。会完成按字节的值拷贝(浅拷贝)。

也就是说有些类,我们是不需要去实现拷贝构造和operator=,因为编译器默认生成的就可以用

比如:Date类就是这样,但是Stack类就不可以(因为需要深拷贝)

(五)取地址运算符重载

一般不用自己写,编译器自动生成的足够我们使用。

Date* operator &()
{
    cout << "Date* operator &()" << endl;//用于测试是否被调用
    return this;
}

//调用
Date d1;
cout << &d1 << endl;

(六)const取地址运算符重载

一般不用自己写,编译器自动生成的足够我们使用。

const Date* operator &() const
{
    cout << "const Date* operator &() const" << endl;//用于测试是否被调用
    return this;
}

//调用
const Date d2;
cout << &d2 << endl;

三、关于类中的一些细节

(一)关于构造函数的一些细节

1.初始化列表

我们之前一直这样写构造函数, 这种构造函数调用之后,对象中就已经有了一个初始值,但是这

个过程不能称作对象的初始化。这样写只能叫做赋初值,而不能叫做初始化。

因为初始化只能初始化一次,而在构造函数内可以多次赋值(显然第一次赋值就是赋初值)

Date(int year = 0, int month = 1, int day = 1)
{
    //函数体内赋值
    _year = year;
    _month = month;
    _day = day;
}

写成初始化列表就是初始化了,具体语法格式如下

Date(int year = 0, int month = 1, int day = 1)
    :_year(year)
    ,_month(month)
    ,_day(day)
{}

现在就有疑问了,那我保证在函数体内只赋值一次不就好了?为什么非要搞出一个初始化列表?

解释:①引用成员变量、const成员变量、没有默认构造函数的自定义类型的成员变量

上述三种成员变量必须进行初始化,所以就一定需要初始化列表!!!

②建议优先使用初始化列表,因为初始化的效率比赋值的效率高。

注意⚠️声明和初始化列表的顺序要保持一致,这样可以减少出错!

//最好要顺序一致,如下所示
class Test
{
public:
    Test(int a1, int a2)
    :_a1(a1)
    ,_a2(a2)
    {}
    
private:
    int _a1;
    int _a2;
};

//尽量不要这样
class Test
{
public:
    Test(int a1, int a2)
    :_a2(a2)
    ,_a1(a1)
    {}
    
private:
    int _a1;
    int _a2;
};

2.explicit关键字

要想学习explic关键字,就要先辨别这三种写法的区别。

(1)单参数隐士类型转换【第三种写法才是】

class Date
{
public:
    Date(int year)
    :_year(year)
    {}
private:
    int _year;
    int _month;
    int _day;
};

//三种写法
Date d1(1);//构造函数
Date d2 = d1;//拷贝构造
Date d3 = 3;//单参数隐士类型的转换 构造出tmp(3)+在用tmp拷贝构造d3(tmp)

(2)C++11支持的多参数隐士类型转换【第三种写法才是】

class Date
{
public:
    Date(int year ,int month ,int day)
    :_year(year)
    ,_month(month)
    ,_day(day)
    {}
private:
    int _year;
    int _month;
    int _day;
};

//三种写法
Date d1(2023,11,18);//构造函数
Date d2 = d1;//拷贝构造
Date d3 = {2023,11,19};//多参数隐士类型的转换

我们看见第三种写法很奇怪,要是不想出现这种隐士类型转换,就在构造函数前加上explicit

也就是说在构造函数前加上explicit,就不可以出现第三种写法了。

class Date
{
public:
    explicit Date(int year)
    :_year(year)
    {}
private:
    int _year;
    int _month;
    int _day;
};

Date d1(1);//构造函数
Date d2 = d1;//拷贝构造
//Date d3 = 3;//隐士类型转换(加上explicit就报错了)

(二)友元

友元(friend)是一种访问私有和保护的方式:正常情况下我们在类外面访问不了私有和保护,

但是如果我们成为朋友,那我就可以访问你的私有和保护了。

1.友元函数

(1)相关知识

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

②友元函数不能用const修饰。

③友元函数可以在类的任何地方声明,不受访问限定符的限制。

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

(2)应用:重载<< 和 >>

不使用友元函数同样可以重载<< 和 >>,那我们为什么要使用友元呢?

不使用友元,在类里面重载<< 和 >>,使用的时候只能这样:d1 << cout,不符合习惯。

class Date
{
public:
    Date(int year = 0, int month = 0, int day = 0)
    {
        _year = year;
        _month = month;
        _day = day;
    }

    // <<这个运算符有两个操作数:一个是cout(他是ostream类型的);另一个是Date
    //这样写可以重载<<,但是由于成员函数默认的第一个参数是this指针
    //所以使用时只能这样使用d1 << cout,不符合习惯
    void operator <<(ostream& out)
    {
        out << _year << "-" << _month << "-" << _day << endl;
    }

private:
    int _year = 0;
    int _month = 1;
    int _day = 1;
};

使用友元,就可以将ostream类型的参数作为第一个操作数,就可以复合习惯的使用。

class Date
{
    friend ostream& operator <<(ostream& out,const Date& d);
    friend istream& operator >>(istream& in,Date& d);
}

//返回值类型是ostream才可以输出多个,实现这种效果 cout << d1 << d2
ostream& operator <<(ostream& out,const Date& d)
{
    out << d._year << "-" << d._month << "-" << d._day << endl;
    return out;
}

//返回值类型是istream才可以输入多个,实现这种效果 cin >> d1 >> d2
istream& operator >>(istream& in,Date& d)
{
    in >> d._year >> d._month >> d._day;
    return in;
}

2.友元类

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

class Time
{
    friend class Date; //声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
public:
   Time(int hour = 0, int minute = 0, int second = 0)
       : _hour(hour)
       , _minute(minute)
       , _second(second)
       {}
private:
    int _hour;
    int _minute;
    int _second;
};

class Date
{
public:
    Date(int year = 1900, int month = 1, int day = 1)
    :_year(year)
    ,_month(month)
    ,_day(day)
    {}
    void SetTimeOfDate(int hour, int minute, int second)
    {
        //直接访问时间类私有的成员变量
        //因为现在Date是Time的朋友,所以Date可以访问Time的私有和保护
        _t._hour = hour;
        _t._minute = minute;
        _t._second = second;
    }
private:
    int _year;
    int _month;
    int _day;
    Time _t;
};

(三)const成员

1.const修饰引用(常引用问题)

权限缩小和放大规则:适用于引用和指针

权限放大:const不能给非const,const只能给const

权限缩小:非const既可以给非const,也可以给const

2.const修饰类的 成员函数

const修饰类的 成员函数,本质上是修饰该成员函数隐含的this指针,表明在该成员函数中不能

对类的任何成员变量进行修改。

(1)对象调用 const成员函数

const对象不可以调用非const成员函数。权限放大:const->非const

class Test
{
public:
    void f1() const
    {}
    
    void f2()
    {}
};

const Test t2;
t2.f1();//const对象可以访问const成员函数
//t2.f2();//const对象不可以访问非const成员函数

非const对象可以调用const成员函数。权限缩小:非const->const

class Test
{
public:
    void f1() const
    {}
    
    void f2()
    {}
};

Test t1;
t1.f1();//非const对象可以调用const成员函数函数
t1.f2();//非const对象可以调用非const成员函数函数

(2)成员函数调用 const成员函数

const成员函数不可以调用非const成员函数。权限放大:const->非const

//f3 f4属于放大行为 不行
void f3()//void f1 (date* this)
{}

void f4()const//void f4(const date* this)
{
    f3();//this->f3(this)
}

非const成员函数可以调用const成员函数。权限缩小:非const->const

//f1 f2属于缩小行为 可以
void f1()//void f1 (date* this)
{
    f2();//this->f2(this)
}
void f2() const
{}

(四)static成员

1.static成员变量

static成员变量 不存在对象中,而是存在静态区。他属于这个类的所有对象,也属于这个类本身。

2.static成员函数

(1)static成员函数,没有this指针,不使用对象就可以调用。

          -> 类名::func() 【⚠️这种方式只能调用静态成员函数】

(2)static成员函数,不能访问 类中的非静态成员(成员变量+成员函数)

但是非静态成员函数可以访问静态成员(成员变量+成员函数)

①静态成员函数不能访问 非静态成员变量 和 非静态成员函数

总结:静态成员函数 只能访问静态的(成员变量+成员函数)

class Test
{
public:
    void f1()
    {}
    
    static void f2()
    {
        //f1();//静态成员函数不能访问非静态成员函数
        //_a = 10;//静态成员函数不能访问非静态成员变量
    }
    
private:
    int _a;
    static int _n;
};

②非静态成员函数可以访问 静态成员变量 和 静态成员函数

总结:非静态成员函数 静态和非静态(成员变量+成员函数)都能访问

class Test
{
public:
    static void f3()
    {}
   
    void f4()
    {}
    
    void f5()
    {
        f3();//非静态成员函数可以访问静态成员函数
        _n = 100;//非静态成员函数可以访问静态成员变量
        
        f4();//非静态成员函数可以访问非静态成员函数
        _a = 10;//非静态成员函数可以访问非静态成员变量
    }
    
private:
    int _a;
    static int _n;
};

四、理解封装

1.数据和方法定义到一起。

2.把想给你看到的数据给你看,不想给你看到的封装起来。(通过三种访问限定符实现)

3.一般成员变量为私有,成员函数为公有。我们只能通过接口函数去改变数据。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值