C++:类和对象(上)

目录

类和对象

1.什么是类

2.类实例化出对象

3.对象大小 

4.隐式this指针

类的默认成员函数

1.构造函数。

2.拷贝构造函数

3.析构函数

4.赋值重载

5.小总结

6.const成员函数与取地址重载函数


类和对象

1.什么是类

C++在C语言的基础上增加了面向对象编程,与之密切相关的概念就是类与对象

类(class),是一种用户自定义的数据类型,可以封装数据和函数

类中的数据类的成员变量或者类的属性,类中的函数类的成员函数或者类的方法

类(class)和C语言中的结构体(struct)类似的,都可以封装数据,但是结构体封装不了函数,但是类可以。类将变量和方法封装在一起的设计明显是更利好的,在一些数据结构,如栈、对队列等就能很好体现优势

多说无益,下面举个例子:

//关键字:class
class cuboid//类名:长方体
{
public://访问限定符
	//成员函数
	int volume(int length, int breadth, int height)//计算长方体的体积
	{
		return length * breadth * height;
	}

private:
	//成员变量
	int _length;//长
	int _breadth;//宽
	int _height;//高
};

通过上面的例子,我们可以看到,相较于结构体,类还多出个访问限定符,其作用是确定类成员的访问属性

访问限定符的域:从该限定符到下一限定符 或 从该限定符到 } 即类结束。

访问限定符有3个:

1.public:公共成员,在类的外部可直接访问的。

2.privtae:私有成员,在类的外部不可直接访问。

3.protected:受保护的成员,在类的外部也是不能直接被访问。

一般来说,成员变量会被privateprotected限制,可以公共使用的成员函数public修饰。


值得一提的是,在C++中,strcut也被升级成了类,也可以封装数据和函数。

两者的一点不同是,在不写访问限定符的情况下,class的成员默认为private,而struct默认为public

需要注意的两点:

1.类名可以代表类型这样写代码可以方便。

2.在类中,如果函数声明和函数定义分离函数定义要用域作用限定符

2.类实例化出对象

可以说一种模板,可以用来创建具有相同属性和方法的多个对象。更形象地说,类就像现实生活中飞机轮船火车的设计图纸,工人们可以根据图纸造出很多飞机轮船火车。

类在内存创建对象,就叫做实例化对象

值得注意的是,在类中,成员变量只是被声明,没有开空间;只有实例化对象的时候,成员变量才会被定义,然后分配空间。

 

3.对象大小 

由类实例化出来的对象大小计算是遵循C语言结构体内存对齐规则的。

想了解内存对齐的可以看我这篇博客。

C语言——结构体-CSDN博客

既然对象大小是按内存对齐规则来算的,也就是说对象大小只跟成员变量有关,而与成员函数无关。那这是为什么呢?

我们不妨想一想,由同一类实例化出的不同对象,每个对象的成员函数都是一样的,有必要每个对象都存个函数指针吗?

完全没必要,不同对象调用成员函数时,调用的其实都是同一个成员函数,类的成员函数都是存放在公共代码区不存放在对象里。所以对象大小只跟成员变量有关,而与成员函数无关。


 我们再来看以下两种特殊情况:

当类中没有成员变量或者什么都无时,实例化出的对象大小都是1个字节

这里1个字节是为了标识对象存在,如果一个字节都没,那怎么表示对象存在。

4.隐式this指针

cuboid类中有Init成员函数,这些成员函数存放在公共代码区,那当box1调用Init函数时,怎样区别Box1和Box2这两个对象?换句话说,你传的参数(1,2,3)是给Box1初始化还是给Box2初始化?

C++给出的解决方案是,每个成员函数调用后,在编译器编译后,都会在函数形参的第一个位置,增添一个当前类型的隐式指针this,指代当前对象

Init函数真正的原型是void Init(cuboid* const this, int x, int y, int z);

函数里成员变量的访问本质也是通过this指针访问

值得注意的是,C++规定了在函数实参形参位置不能显示地写this指针,编译器会帮我们处理,但在函数体内可以使用this指针(可以写,也可以不写,为了方便,一般都不写)

类的默认成员函数

默认成员函数:我们没有显式实现,编译器会自动生成的成员函数一个类,我们不写的情况下,编译器会生成6个默认成员函数,前4个较为重要。

在C++11中又增添了2个默认成员函数,移动构造和移动赋值。

1.构造函数。

构造函数是一种特殊的成员函数,虽然名叫构造函数,但是它的作用是完成类对象的初始化,类似于Stack的Init函数。

构造函数不是开辟空间创建对象,局部对象在栈帧创建时,空间就开好了。

其特点:

1.函数名类名相同无返回值。(C++规定不需要写void

2.实例化对象时系统自动调用对应构造函数。如果我们没有写构造函数,编译器会自动生成一个无参数的默认构造函数,我们写了就不会生成。

3.默认构造函数:无参构造函数,全缺省构造函数,不写时编译器生成的构造函数都叫默认构造函数三个只能存在其一;所以默认构造函数既包括编译器自动生成的,也包括我们自己写的,这点不要混淆;

总的来说,不传参就能调用的函数就叫默认构造函数

4.对于编译器自动生成的构造函数,对于内置类型的变量,它的初始化是不确定的,也就是说变量可能是随机值,也可能是一个具体的值;对于自定义类型的变量(用class、struct定义的类型),会调用该类型变量的默认构造函数,如果没有,编译器就会报错,要初始化该变量,需用初始化列表。

class A
{
public:
    //1.无参构造函数
    /*A()
    {
        _a = 1;
        _b = 2;
    }*/

    //2.带参构造
    /*A(int a, int b)
    {
        _a = a;
        _b = b;
    }*/

    //3.全缺省构造 1和2可以写成全缺省
    A(int a = 3, int b = 4)
    {
        _a = a;
        _b = b;
    }

    void Print()
    {
        cout << _a << endl;
        cout << _b << endl;
    }
private:
    int _a;
    int _b;
};

1 2构造函数与3构造函数只能存在其一,同时存在会有函数调用的歧义,一般写构造函数都是写全缺省构造


无参构造函数创建对象时对象后面不用跟括号,否则编译器无法区分这里是函数声明还是实例化对象

这一点需要注意一下。

2.拷贝构造函数

拷贝构造函数是一种特殊的构造函数,是构造函数的一个重载,由编译器调用来完成一些基于同一类的其他对象的构建及初始化。

说简单点就是,一个已经存在的对象帮另一个对象初始化这是个重点。

1.C++规定自定义类型对象进行拷贝行为必须调用拷贝构造传值传参和传值返回)。

2.拷贝构造的第一个参数必须是类类型的引用,如果是传值的方式编译会报错,因为语法上会导致无穷递归调用。而原因就是在C++中,传值调用会调用拷贝构造,那如果拷贝构造也是传值调用,那就无穷递归了(传值调用->拷贝构造->传值调用->拷贝构造......)

这里的参数不是类类型的引用,编译器就报错了。

3.如果没有显示写拷贝构造函数,编译器会自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量完成浅拷贝,对自定义类型成员变量会调用它的拷贝构造函数

一般来说,类中的成员变量都是内置类型,没有资源指向在堆上new了空间),就不需要我们写拷贝构造。如果有资源指向,就需要显示写拷贝构造

class A
{
public:
    A(int a = 3, int b = 2)
    {
        _a = a;
        _b = b;
    }

    A(const A& aa)    //拷贝构造
    {
        _a = aa._a;
        _b = aa._b;

    }

    void Print()
            {
                cout << _a << ' ' << _b << endl;
                
            }

private:
    int _a;
    int _b;
};

int main()
{
    A aa1;
    A aa2(aa1);
    A aa3 = aa1;   //拷贝构造也可以这样写
    aa1.Print();
    aa2.Print();
    aa3.Print();
    return 0;
}

3.析构函数

析构函数与构造函数作用相反,它的工作是完成对资源的清理工作,对象在销毁时会自动调用析构函数。

1.析构函数在类名前加~无参数无返回值(跟构造类似,不加void)。

2.一个类只能有一个构造函数,在对象生命周期结束后会自动调用,若我们没有写析构编译器会自动生成

自动生成的析构函数内置类型成员不做处理自定义的会调用它的析构函数

但是我们显示写了析构函数自定义类型也会调用它的析构函数

无论什么情况,自定义类型的成员都会调用它的析构函数。

3.有资源申请时,就应该显示写析构函数一定要写,不然资源会泄露。

4.局部域中的多个对象,后定义的对象先析构。

class A
{
public:
    A()
    {
        _a = new int[10];  //申请资源
    }

    ~A()              //完成对资源的释放
    {
        delete[] _a;
    }
private:
    int* _a;
};

4.赋值重载

有关运算符重载:

运算符被用于类类型的对象时,C++允许我们通过运算符重载为其指定新的含义;

类类型对象使用运算符时必须转换对应的运算符重载,如若没有,编译器会报错。

运算符重载是特殊的函数,其名字由operator和后面定义的运算符共同构成,有返回类型和参数列表,运算符重载后,其优先级和结合性与原先的运算符保持一致运算符重载不能重载语法中没有的符号来创建新的操作符,如:operator@。

重载运算符函数的参数个数和该运算符作用的运算对象数量⼀样多。一元运算符有一个,二元运算符有两个参数(左侧运算对象传第一个参数,右侧传第二个)。但若该函数为类的成员函数,则参数比运算对象少一个,因为类的成员函数第一个参数是隐式的this指针,也就是说第一个运算对象就是this指针(该对象)。

.*  ::  sizeof  ?:  . 以上5个运算符不能重载。

一个类需要重载哪些运算符,得看重载后这些运算符有没有意义。比如日期类Date重载operator-就有意义,重载operator+就没有意义(日期相减得相差天数,但是日期相加没有意义)

重载++运算符时,有前置++和后置++,前置++ operator++()后置++ operator(int),后置++增加一个int形参以方便区分。

重载流插入<<和流提取>>时,需要重载为全局函数。如果重载为成员函数,this指针占据了第一个形参的位置,第一个行参位置是左侧运算对象,调用时变成了 对象<<cout,不符合使用习惯,影响可读性。重载为全局函数时第一个参数应该是ostream//istream第二个参数为类类型对象。


 赋值重载:

赋值重载函数也是一个默认成员函数,用于完成两个已经存在的对象进行直接拷贝赋值,这里要跟拷贝构造进行区分,拷贝构造是一个已经存在的对象给另一个要创建的对象初始化。

1.赋值重载规定必须重载为成员函数,原因是赋值运算符在类中不显式实现时,编译器会生成一份默认的,此时用户在类外再将赋值运算符重载为全局的,就和编译器生成的默认赋值运算符冲突了,故赋值运算符只能重载成成员函数。

所以无论如何,类里都有赋值重载函数(编译器生成的或者我们显示写的)。

2.编译器自动生成的赋值重载函数对内置类型成员变量进行浅拷贝(一个字节一个字节的拷贝),对自定义类型会调用它的拷贝构造函数。

3.赋值重载函数的参数建议写成const 类型的引用,以此避免拷贝。同理,其返回值也建议写成类类型引用(有返回值是为了支持连续赋值)。

Date& operator=(const Date& d)
{
	// 检查⾃⼰给⾃⼰赋值的情况        
	if (this != &d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	// d1 = d2表达式的返回对象应该为d1,也就是*this
	return *this;
}

5.小总结

   在我们实现一个类的时候,构造函数能写就写,而拷贝构造函数、析构函数、赋值重载函数这三个函数是绑定的,当类中有资源指向时,这三个函数就必须显示写出来。(有资源指向时,这三函数一定要写,不然就会出很多bug)    

6.const成员函数与取地址重载函数

const修饰的成员函数称之为const成员函数const修饰成员函数放到成员函数参数列表的后 ⾯。(一种非常奇怪的格式)

const成员函数的作用让类中的成员变量在成员函数中不能被修改,也就让原本隐式this指针Date* const this 变为 const Date* const this(以日期类Date为例子)

// void Print(const Date* const this) const
 void Print() const
 {
 cout << _year << "-" << _month << "-" << _day << endl;
 }

取地址重载函数

取地址重载分为普通对象取地址重载const对象取地址重载,一般来说编译器自动生成的就够用,不需要显示实现。

除非在一些特殊情况下,比如我们不想让别人得到当前对象的地址,就自己实现一份,胡乱返回个地址。

class Date
{
public:
	Date* operator&()
	{
		return this;
		// return nullptr;
	}
	const Date* operator&()const
	{
		return this;
		// return nullptr;
	}
private:
	int _year; 
	int _month; 
	int _day; 
};

拜拜,下期再见😏

摸鱼ing😴✨🎞

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值