C++类和对象以及默认成员函数

本文详细介绍了C++中类和对象的概念,包括它们与C语言的区别,访问限定符的应用,类的实例化、大小、构造函数(包括默认构造函数和拷贝构造)、析构函数、赋值运算符重载、初始化列表以及自定义类型转换。还讨论了如何使用explicit关键字控制隐式类型转换。
摘要由CSDN通过智能技术生成

前言

        C++和C语言之间最大的区别就是C++出现了类和对象。C语言是面向过程的,就好比你想吃饭那么你只能自己亲手做饭吃,切菜,下锅,炒菜,出锅这些事都需要自己去完成。但是在C++中是面向对象的,就好比你去店里吃饭,你只管下单,然后复杂的做法过程你全程不管交给厨师去完成,你只管厨师把饭做好端给你吃。这其中就C++就好比把做法的过程封装了起来变成厨师,你想吃饭的时候就可以让厨师去做,而不用你自己去做饭。

类和对象

struct和class

        类里定义的函数默认 inline 修饰,成员函数声明和定义分离,要在定义前面用域作用限定修饰符修饰类域。

        C++兼容C语言,并且把结构体 struct 升级成了类。类中的变量称为成员变量,还可以定义成员函数。

struct Class
{
    //成员函数
    void Init()
    {};

    //成员变量
    int a;
    int* b;
};

        C++增加了 class,class 中也可以定义成员变量和成员函数。

class Class
{
    //成员函数
    void Init()
    {};

    //成员变量
    int a;
    int* b;
};

访问限定符

        访问限定符有三种分别是 public(公有),protected(保护)、private(私有)。

public修饰的成员在类外可以之间被访问

protected和private修饰的成员在类外不能直接被访问

class的默认访问权限为private,struct为public

类的实例化

        下面定义了一个类 MyClass,这是这个类的声明。那么类里面的那些成员是声明还是定义呐?

class MyClass
{
public:
	void MyFunc(int a)
	{
		cout << a + _b << endl;
	}
private:
//成员变量最好使用_作为标识,防止混淆
	int _b = 1;
};

         在调用这个类的时候会为这个类和里面的成员变量开辟内存空间,这个时候就是类的实例化(对象的定义)。类定义里的成员都是声明,只有在调用类后为这个成员开辟了内存空间,这个时候成员才是定义。

int main()
{
	MyClass Class;
	Class.MyFunc(2);
	return 0;
}

 类的大小

        C语言中结构体 struct 的大小只算变量大小然后字节对齐(对齐数:编译器默认对齐数与成员大小的较小值)就可以算出,那么C++中类的大小该怎么算呐?毕竟类中还可以定义函数。

int main()
{
	MyClass Class;
	Class.MyFunc(2);
	cout << sizeof(Class) << endl;
	return 0;
}

         上面的代码 cmd 给出的回复是 4,也就是说类的大小和结构体的大小计算方式应该是一样的。但是类里面的函数哪去了呐?类中的成员函数放到了公共代码区,每次调用函数的时候就去公共代码区里面调用。

没有成员变量的类

class Test1
{
	;
};

class Test2
{
	int Sun(int a, int b)
	{
		return a + b;
	}
};

int main()
{
	Test1 A;
	Test2 B;
	cout << sizeof(A) << endl;
	cout << sizeof(B) << endl;
	return 0;
}

        上面两个类都没有成员变量,按之前的类大小计算来说这里的cmd输出应该是0,但事实上cmd输出的是1。这里就要说一下,没有成员变量的类对象需要1比特,这是为了占位,表示对象存在,不存储有效数据。

This指针

class Date
{
public:
	void Init(int year = 2024, int month = 2, int day = 28)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Printf()
	{
		cout << "Date:" << _year << " " << _month << " " << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date A;
	A.Init();
	A.Printf();
	return 0;
}

         在 A.Printf() 的时候会去调用 Printf 这个函数,但是我们并没有传参函数就自己去调用变量。这是怎么做到的呐?这是因为编译器会对成员函数做一些处理。

void Printf(Date* const this)
{
	cout << "Date:" << this->_year << " " << this->_month << " " << this->_day << endl;
}
int main()
{
    A.Printf(&A);
    return 0;
}

        转化为类似上面的代码。省去了我们自己手动去调用类 A,编译器会帮我们去调用这个类。我们不能自己手动显式的去调用,但是我们可以在函数内部显式使用 this 指针。this 指针是形参在函数调用创建栈帧时创建。

void Printf()
{
	cout << "Date:" << this->_year << " " << this->_month << " " << this->_day << endl;
}

类的默认成员函数

构造函数和析构函数

        构造函数是特殊的成员函数,负责初始化对象。构造函数有以下几个特性:

#1 构造函数的函数名与类名相同
#2 构造函数没有返回值也不需要写void
#3 对象实例化时编译器自动调用对应的构造函数
#4 构造函数可以重载
#5 没有显示定义构造函数,编译器会自己生成一个无参的默认构造函数
   一旦用户显式定义了构造函数则编译器将不再生成
#6 编译器默认生成的构造函数,内置类型不做处理,自定义类型会去调用它的默认构造
   一般情况下有内置类型的类需要自己写构造函数,全是自定义类型的类可以用编译器的构造函数
#7 C++11针对 #5 打了一个补丁,内置类型可以给缺省值
#8 不传参就可以调用的构造函数就是默认构造函数

        析构函数与构造函数功能相反,负责在对象销毁时完成对象中资源的清理工作(防止内存溢出)。析构函数有以下几个特性:

#1 析构函数是在类名前面加上字符"~"
#2 无参数无返回值类型
#3 一个类只能有一个析构函数,如果未显示定义
   系统会自动生成默认析构函数,析构函数不能重载
#4 对象生命周期结束时,编译器会自动调用析构函数
#5 编译器默认生成的析构函数,内置类型不做处理,自定义类型会去调用它的默认析构
#6 一般有动态申请空间的变量就需要写析构函数
#include <assert.h>
class SeqList
{
public:
	SeqList(int capacity = 4);
	~SeqList();
private:
	int* _data;
	int _size = 4;
};

SeqList::SeqList(int capacity)
{
	int* New = (int*)malloc(sizeof(int) * capacity);
	assert(New);
	_data = New;
	_size = capacity;
}

SeqList::~SeqList()
{
	assert(_data);
	free(_data);
	_data = nullptr;
	_size = 4;
}

int main()
{
	SeqList S1(10);
	return 0;
}

拷贝构造函数

        拷贝构造函数是特殊的成员函数,它的特性如下:

#1 拷贝构造函数是构造函数的一种重载形式
#2 拷贝构造函数的参数只有一个且必须是类类型对象的引用
   使用传值方式编译器直接报错,因为会引发无穷递归调用
#3 若未显式定义,编译器会生成默认的拷贝构造函数
   内置类型成员完成值拷贝,自定义类型会调用它的拷贝构造
class SeqList
{
public:
	SeqList(int capacity = 4);
	~SeqList();
    SeqList(const SeqList& S);
private:
	int* _data;
	int _size = 4;
};

SeqList::SeqList(const SeqList& S)
{
	_data = S._data;
	_size = S._size;
}

int main()
{
    SeqList S1(10);
	SeqList S2(S);
	return 0;
}

        上面这个拷贝构造函数是有问题的,问题在于_data = S._data 这只是一个浅拷贝。这相当于把 S2 的 _data 指向 S1 的 _data 就导致 S1和S2 使用的是同一块空间,没有达成拷贝的目的。

        要实现这个类的拷贝构造得使用深拷贝完成。

SeqList::SeqList(const SeqList& S)
{
	int* New = (int*)malloc(sizeof(int) * (S._size));
	assert(New);
	for (int i = 0; i < S._size ; i++)
	{
		*(New + i) = *(S._data + i);
	}
	_data = New;
	_size = S._size;
}

赋值运算符重载

运算符重载

        C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有返回值类型。

函数原型: 返回值类型 operator操作符(参数)

#1 不能通过连接其他符号来创建新的操作符
#2 重载操作符必须有一个类类型参数
#3 用于内置类型的运算符,其含义不能改变
#4 作为类成员函数重载时,形参中隐藏有一个this指针
#5 操作符是几个操作数,重载函数就有几个参数
#6 .* :: sizeof ?: . 这个5个运算符不能重载
//定义日期类
class Date
{
public:
	//构造函数
	Date(int year = 0, int month = 0, int day = 0);
	//析构函数编译器生成
	//拷贝构造函数编译器生成

	//运算符<重载
	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;
		}
		else
		return false;
	}

private:
	int _year;
	int _month;
	int _day;
};

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

int main()
{
	Date D1(2024, 3, 3);
	Date D2;
	cout << (D1 < D2) << endl;
	return 0;
}

赋值运算符重载

#1 默认生成的赋值重载模板

class Class
{
    &Class operator=(const Class& a)
    {
        if (this != &a)    //判断是否是自己赋值自己
        {
            ...;
        }
        return *this;    //引用返回是为了连续赋值
    }
};

#2 默认生成的赋值重载跟拷贝构造行为一样
   内置类型浅拷贝,自定义类型去调用它的赋值重载
#3 赋值运算符只能重载成类的成员函数不能重载成全局函数
   (怕与编译器生成的赋值运算函数冲突)

        赋值运算符重载主要用于已经存在的两个对象之间复制拷贝,用一个已经存在的对象初始化另一个对象,类似下面这个函数。

class Date
{
public:
	void operator=(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
};

        但是赋值运算符还有这样的场景

int a = b = c = 1;
a = a;

          这个场景之前的函数就没办法完成,需要改进。

class Date
{
public:
	Date& operator=(const Date& d)
	{
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}
};

取地址操作符重载和const修饰的取地址操作符重载

        我们通常可以对一个对象进行取地址。当对象是内置类型的时候编译器可以自动识别类型进行取地址,当对象是自定义类型的时候取地址符号 (&) 识别不了,需要我们写一个运算符重载函数去实现这个功能。当然取地址操作符重载和const修饰的取地址操作符重载是类的默认成员函数,就算我们不写,编译器也会自己帮我们写。

class Date
{
public:
	Date* operator&()
	{
		return this;
	}

	const Date* operator&() const
	{
		return this;
	}

private:
	int _year;
	int _month;
	int _day;
};

初始化列表

        在创建对象时,编译器会调用构造函数对对象进行初始化工作。对象的初始化有两种方式,一种是构造函数体赋值另一个是初始化列表。

构造函数体赋值

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

初始化列表

        初始化列表以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。

#1 每个成员变量在初始化列表中只能出现一次(只能初始化一次)
#2 类中包含以下成员,必须放在初始化列表位置进行初始化
    # 引用成员变量
    # const成员变量
        引用和const都是必须在定义的时候初始化
    # 自定义类型成员且该类没有默认构造时
#3 初始化列表的初始化顺序是在类中的声明次序
   与其在初始化列表中的先后次序无关
class Test2
{
public:
	Test2(int a)
	{
		_a = a;
	}
private:
	int _a;
};

class Test1
{
public:
	Test1(int a, int& b, int c, int test)
		:_a(a)
		, _b(b)
		, _c(c)
		, _test2(test)
	{}
private:
	int _a;
	int& _b;
	const int _c;
	Test2 _test2;
};

自定义类型的隐式类型转换

class A
{
public:
	A(int a)
		:_a(a)
	{}
private:
	int _a;
};

int main()
{
	A a(1);
	A b = 1;    //隐式类型转换,整形转换为自定义类型
	return 0;
}

        (A b = 1;) 这一条命令中发生了隐式类型转换,先使用"1"作为参数调用构造函数,再使用拷贝构造拷贝给b。但是通常编译器会在这里进行一个优化,(A b = 1) 就相当于直接调用的构造函数,不再去拷贝了。

explicit

class A
{
public:
	explicit A(int a)
		:_a(a)
	{}
private:
	int _a;
};

        在构造函数前使用 explicit 修饰就可以不允许这个类进行隐式类型转换。

  • 7
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值