首页 > 编程语言 >[C++]类和对象(上篇)

[C++]类和对象(上篇)

时间:2024-11-10 23:43:44浏览次数:7  
标签:函数 对象 C++ 重载 int year Date 构造函数

类和对象

  • 通常情况下采用class定义(默认private)
  • 由于C++向下兼容的特性,也可采用struct定义类(默认public)
class Func //默认都是私有,private,外部函数不能通过成员名直接访问
{
	int _x;
	int _arr[10];
};

public Func // 默认都是公共,public,外部函数能通过成员名直接访问
{
	int _x;
	int _arr[10];
};

①类的声明与定义

  1. 成员函数的类声明放在头文件(.h),成员函数定义放在源文件(.cpp)
  • 注意,如果成员函数在类里面定义,将会被认为是定义内联函数
  • 因此当成员函数在类里面定义时,不要声明与定义分离

②成员变量的规范化

  • 查看以下代码的问题:
class Date
{
public:
 void Init(int year)
 {
 year = year;
 }
private:
 int year;
};

这段代码中,Init函数中的参数是year,而成员变量也是year,会导致编译器不确定访问哪一个year,因此需要对成员变量的名称规范化

private:
	int _year;
  • 通常情况下在变量前加一个杠即可,具体看地方要求或是个人喜好

1. 成员变量为声明

  • 类中的成员变量都是声明,没有实际开空间,需要类实例化对象去使用

类似图纸,建筑工程中的图纸就是成员变量,只是一个模板声明,实际建成的房屋就是类实例化对象,才能入住使用。

③计算类的大小

  1. 计算下面类的大小
class A
{
public:
	void f1(){}
private:
	int _a;
	char _ch;
};
  • 计算类的大小与结构体计算方法类似,先找到对齐数(比较默认对齐数当前成员变量中最大的变量,取两个中的最小值,默认对齐数一般而言是8个字节)

  • 计算类时,不看成员函数,只看成员变量

  • 这里类的大小为4

  1. 当类中没有成员变量时:

在这里插入图片描述
类的大小为1。没有成员变量的类对象,需要1byte,是为了占位,表示对象存在

1. 对齐数的意义

  • 假设:硬件在正常情况下,每次搜索的字节为4。并且修改上面的代码:
class A
{
public:
	void f1(){}
private:
	char _ch;
	int _a;
};

某次访问类时,如果类没有按照对齐规则存储,而是:

如果按照这样的方式存储,那么为了读完_a的数据,CPU将会连续读取2次数据。对于_a而言,就是让CPU读了整整两次(对于_ch没什么影响)

某次访问类,如果按照对齐规则存储,则:

对于_a而言,只需要让CPU读取一次就能获得数据

  • 因此内存对齐是通过多开了内存,花费更多的空间使得数据读取速度更快:
  • 典型的空间换时间

也有办法让内存不对齐,一般在嵌入式这种对内存有限制的环境下可能会用到。

2. 错题展示


在这里插入图片描述

④成员函数的位置以及传参细节

1. 成员函数的位置

  • 当调用成员函数时,实际上实在应该公共区域去调用而非类内部,这样做的好处是:
  1. 当有有个类实例化对象要调用成员函数时,就不需要到类里面重新展开函数使用。如果有一百个类的实例化对象调用了成员函数,那么就要展开一百个这样的成员函数,非常麻烦。因此我们把成员函数放在一个公共区域,每次调用时就直接call公共区域的成员函数,用完了后就放在原地等待下一个调用…
  2. 这也解释了为什么计算类大小时不考虑成员函数,因为成员函数都不在类里面。

2. 传参细节

  • 给出一段代码:
class Date
{
public:
	int _year;
	int _month;
	int _day;
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

现在创建了两个类实例化对象,并调用了Print成员函数:

int main()
{
	Date d1, d2;
	d1.Init(2022, 1, 11);
	d2.Init(2022, 1, 12);
	d1.Print(); // 输出结果:2022 1 11
	d2.Print(); // 输出结果:2022 1 12
    return 0;
}
  • 思考:既然成员函数是在”公共区域“而非类里面,那当我们调用这个函数的时候,明明Print()函数中没有传值,那为什么它会知道调用者(类实例化对象)的成员变量呢?

事实上,调用成员函数时,会把类实例化对象的地址传给函数,所以Print函数应该是这样的

Print(Date* const this)//这段代码不能明写出来,但是可以在函数内部用this指针

注意:this不能在形参和实参显示传递,但是可以在函数内部显示使用

		cout << this << endl;
		cout << this->_year << "-" << _month << "-" << _day << endl;

结果与上面的代码是一致的。

  • this实际上是一个形参,调用时会压栈,因此this的位置是在栈区

this指针

  1. this指针是隐含的传参,是调用函数的成员的指针
  2. 只能生成于非静态区的成员函数对象
  3. this指针的原型是Date* const this,所以this本身是不能修改的

现给出两段代码,判断能否运行或是运行错误的原因:

// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
 void Print()
 {
 cout << "Print()" << endl;
 }
private:
 int _a;
};
int main()
{
 A* p = nullptr;
 p->Print();
 return 0;
}
// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{ 
public:
    void PrintA() 
   {
        cout<<_a<<endl;
   }
private:
 int _a;
};
int main()
{
    A* p = nullptr;
    p->PrintA();
    return 0;
}

第一题:C
第二题:B
这道题目说明:this可以为空指针进入成员函数,但是不能直接解引用this,因为对空的引用是错误的

⑤类的6个默认成员函数

1. 构造函数

概念
  • 构造函数主要是完成初始化的工作,在函数创建时就自动执行
特性
  1. 函数名与类名相同
  2. 不需要返回值
  3. 对象实例化时自动调用对应的构造函数
  4. 构造函数可以重载
  5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦
    用户显式定义编译器将不再生成
class Date
{
public:
	// 1.无参构造函数
	Date()
	{}
	// 2.带参构造函数
	Date(int year, int month = 0, int day = 0)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1; // 调用无参构造函数
	//Data d1(); //这种是严重错误的,因为编译器无法分清d1是变量还是函数名
	Date d2(2015, 1, 1); // 调用带参的构造函数
    return 0;
}
无参构造函数与全缺省构造函数不能同时出现
  • 调用无参函数构造时,如果同时出现无参构造函数和全缺省构造函数,由于两者都支持无参传入,会导致编译器不知道该使用哪个函数
class Date
{
private:
	int _year;
	int _month;
	int _day;
public:
	// 1.无参构造函数
	Date()
	{}
	// 2.全缺省构造函数
	Date(int year = 0, int month = 0, int day = 0)
	{
		_year = year;
		_month = month;
		_day = day;
	}
};

int main()
{
	Date d1; // 调用无参构造函数
   return 0;
}

输出结果:

默认构造函数(支持不传参的构造函数)
  • 不传参就能使用的构造函数即默认构造函数
  1. 无参构造函数
  2. 全缺省构造函数
  3. 当没有显示定义时,编译器自动生成的构造函数
  • 如果我们没有创建构造函数,编译器会默认创建构造函数,但是一般不用编译器自动生成的构造函数,因为他不会对内置类型初始化对自定义类型会去调用它的默认构造函数

部分编译器会个性化处理内置类型(int/double…),而正常情况下不应该初始化内置类型,因此在开发中,如果成员对象都是内置类型,最好不使用编译器自带的构造函数

成员对象都是自定义类型(class/struct/union)时,可以考虑使用编译器生成的构造函数

[C++11]成员变量的声明可以给缺省值(意味某些情况下对内置类型无需创建构造函数)
  • C++11中添加了新的特性,对于成员变量的声明可以给予缺省值(成员变量属于声明不是定义)
class Date
{
public:
	// 1.无参构造函数
	Date()
	{}
private:
	int _year = 2024; //声明给予缺省值
	int _month = 11;
	int _day = 9;
};

int main()
{
	Date d1; // 调用无参构造函数
 return 0;
}

是否需要创建构造函数
  1. 需要创建构造函数:

一般情况下,需要自己写构造函数

  1. 不需要创建构造函数:

内置类型成员都有缺省值,且初始化符合我们的要求
成员变量全是自定义类型,且这些类型都定义了默认构造

2. 析构函数

概念
  • 对象在销毁时会自动调用析构函数。完成对象中资源的清理工作
特性
  1. 析构函数名是在类名前加上字符 ~
  2. 无参数无返回值类型。
  3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构
    函数不能重载
  4. 对象生命周期结束时,C++编译系统系统自动调用析构函数
是否需要创建析构函数
  1. 需要创建析构函数

一般情况下,有动态申请资源(malloc…),就需要显示写析构函数释放资源

2.不需要创建析构函数

没有动态申请的资源
需要释放资源的成员都是自定义类型

析构函数的触发条件以及变量的析构顺序

当构造类变量后,如果都在局部内构造,那么析构顺序就是构造顺序的相反(因为栈是先进后出的原则)
例题:


2.

答案:
1.

2.

3.拷贝构造函数

概念
  • 只有单个形参该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用
特性
  1. 拷贝构造函数是构造函数的一个重载形式
  2. 拷贝构造函数的参数只有一个必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用

class Date
{
public:
	// 1.无参构造函数
	Date()
	{}
	// 2.拷贝构造函数
	Date(const Date& d)
	{
		this-> _year= d._year;
		_month= d._month; // this是d2的指针,可以不用this
		this->_day = d._day;
	}
private:
	int _year = 2024;
	int _month = 11;
	int _day = 9;
};

int main()
{
	Date d1;
	Date d2(d1); //调用拷贝构造函数,将d1的值传给d2
    return 0;
}

拷贝构造与构造函数

拷贝构造函数的触发条件
  • C++规定:
  1. 内置类型直接拷贝

  2. 自定义类型必须调用拷贝构造函数完成拷贝

编译器生成的拷贝构造函数与浅拷贝
  1. 若未显示定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝

内置类型成员完成值拷贝/浅拷贝
自定义类型成员会调用他的拷贝构造

浅拷贝带来的问题

如果成员变量中有动态申请的资源,如malloc后的数组_a:

问题:

  1. 析构两次,报错
  2. 一个修改会影响另一个
  • 由浅拷贝引出的其他:
    看以下的问题代码:
Stack ret = func();

Stack& func()
{
	stack st;
	return st;(返回st的别名)
}

解决方法:

Stack func()
{
	stack st;
	return st;
}

Stack& func()
{
	static stack st;
	return st;
}

4. 赋值(运算符)重载

运算符重载
1. 概念
  • C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其
    返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

用法:

返回值类型 operator操作符(参数列表)

2. 注意
  1. 不能通过连接其他符号来创建新的操作符:比如operator@
  2. 重载操作符必须有一个类类型参数
  3. 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义(例如(int)a + (int)b)
  4. 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐
    藏的this
  5. .* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现

全局的operator(如果类的对象是私有的,那么将无法使用该运算符重载

class Date
{ 
public:
 Date(int year = 1900, int month = 1, int day = 1)
   {
        _year = year;
        _month = month;
        _day = day;
   }    
//private:
 int _year;
 int _month;
 int _day;
};
// 这里会发现运算符重载成全局的就需要成员变量是公有的,那么问题来了,封装性如何保证?
// 这里其实可以用我们后面学习的友元解决,或者干脆重载成成员函数。
bool operator==(const Date& d1, const Date& d2)
{
    return d1._year == d2._year
   && d1._month == d2._month
        && d1._day == d2._day;
}
void Test ()
{
    Date d1(2018, 9, 26);
    Date d2(2018, 9, 27);
    cout<<(d1 == d2)<<endl;
}

--------------------------------------------------------------------------------

void Test ()
{
    Date d1(2018, 9, 26);
    Date d2(2018, 9, 27);
    cout<<(d1 == d2)<<endl;
}
class Date
{ 
public:
 Date(int year = 1900, int month = 1, int day = 1)
   {
        _year = year;
        _month = month;
        _day = day;
   }
    
    // bool operator==(Date* this, const Date& d2)
    // 这里需要注意的是,左操作数是this,指向调用函数的对象
    bool operator==(const Date& d2)
 {
        return _year == d2._year;
            && _month == d2._month
            && _day == d2._day;
 }
private:
 int _year;
 int _month;
 int _day;
};
赋值重载
重载格式
  1. 参数类型:const T&,传递引用可以提高传参效率
  2. 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
  3. 检测是否自己给自己赋值
  4. 返回*this :要复合连续赋值的含义(例如,a = b = c(都是int类型),顺序是:c赋值给b,所以b = c的返回值就是b,再将这返回值赋值给a,所以a = b = c 的返回值就是a)

正确示范:

编译器默认生成的赋值重载
  • 与拷贝构造一样,对内置类型不做处理自定义类型调用自身的赋值重载
  • 赋值运算符只能作为类的成员函数重载

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

不需要自己创建赋值重载的情况
  1. 只要求浅拷贝/值拷贝
  2. 成员类型全是自定义类型
拷贝构造与赋值重载的区别(具体情况看具体分析,赋值重载也可以成为拷贝构造)
  1. 拷贝构造函数的定义:一个已经存在的对象初始化另一个对象
  2. 赋值重载的定义:已经存在的两个对象之间复制拷贝
运算符重载补充
重载后置++/后置- -的处理
  • 赋值运算符重载只是特殊的运算符重载,在类内我们依然可以实现其他的运算符重载
  • +=是重载后的运算符,表示对该类的天数加1并返回该类的引用
  • 后置++时,增加的(int)仅是为了占位,构成函数重载
	//前置++
	Date& operator++();

	//后置++
	Date operator++(int); // 使用方法:d1.operator++(&d1, 0)

	//前置--
	Date& operator--();

	//后置--
	Date operator--(int);
-----------------------------------------

Date& Date::operator++()
{
	*this += 1;
	return *this;
}

Date Date::operator++(int)
{
	Date tmp(*this);
	++(*this);
	return tmp;
}

Date& Date::operator--()
{
	*this -= 1;
	return *this;
}

Date Date::operator--(int)
{
	Date tmp(*this);
	--(*this);
	return tmp;
}
重载流输入和流输出

如果我们想要获取一个具体日期,那用已经实现的日期类的方式是这样的:

d1 += 100;
d1.Print();

这段代码看起来让人不适,明明有cout<<x的输出方式我们却要用print,这与c语言里的printf有什么区别?
因此我们可以大胆一点,直接重载<<运算符,实现cout << d1
说做就做,现在就开始重载。我们先尝试用惯性思维,在类里面实现该运算符重载

Date类内:

void operator(ostream& out)
{
	out << _year << "年" << _month << "月" << _day << "日" << endl;
}

主函数内:

cout << d1;

但是这里会发生一个巨大错误

cout << d1 等价于 cout.operator(&d1)
  • 由于操作符的左边是第一个操作数,因此cout会被当成成员对象传参,可实际上cout并不是Date类里的成员,所以会报错

如果单纯想解决这个问题,那么只需要调换两者的位置即可:

d1 << cout;

这难道看起来不奇怪吗?怎么变成了cout流入d1呢?

友元函数
  • 因此,对于重载运算符的操作,不能在类里面实现,而应该在全局里面实现,并且由于全局不能访问类的私有对象,我们还需要用友元函数来声明运算符重载函数从而访问成员变量
重载流输出的方法(缺点:不能连续输出)

既然在全局里实现,那就可以这样定义:

Date.cpp内:

void operator<<(ostream& out, const Date d)
{
	out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
}

头文件:Date类内:

friend void operator<<(ostream& out, const Date d);

public:
	//...

源文件:test.cpp内:

cout << d1;

这样我们就实现了<<的重载

  • 但是,如果出现了cout << d1 << d2 << d3时,运算符会从左到右实现,而由于cout << d1 的返回值是void,那么将会导致后面的 d2 与 d3 都无法输出
重载流输出的标准方法
Date.cpp内:

ostream& operator<<(ostream& out, const Date& d)
{
	out <<d. _year << "年" << d._month << "月" << d._day << "日" << endl;
	return out;
}

头文件:Date类内:

friend istream& operator>>(istream& in, Date& d);//友元函数声明

源文件:test.cpp内:

cout << d1 << d2 << d3;//成功打印

  • 只需要让返回值为ostream&即可连续输出
重载流插入

同上一样操作即可

istream& operator>>(istream& in,  Date& d)
{
	in >> d._year >> d._month >> d._day;
	return in;
}
5.6. 取地址重载
  • 重载&(取地址符号),让对象取到(取不到)自身的地址
	class Date
{
public:
	Date* operator&()
	{
		cout << "Date* operator&()" << endl;
		//return this; 返回自身地址
		return nullptr; 让普通对象取不到地址
	}

	const Date* operator&() const
	{
		cout << "const Date* operator&() const" << endl; 让const常量对象取到地址

		return this;
	}
private:
	int _year = 1;
	int _month = 1;
	int _day = 1;

int main()
{
	Date d1;
	const Date d2;
	
	return 0;
}
const成员
  • const修饰的成员是常量成员:
	const Date d2(2023, 1, 1);
	d2.Print();//报错

-报错的原因很简单:对象权限被放大了:

Print(Date* this(隐式传参)),我们可以看到this是没有const修饰的,但是对于d2来说,d2是不可被修改的类类型,因此this就必须要被修改成:Print(const Date* this)(const放在前面修饰 *this,放在Date *后面修饰this)

  • 但是,由于this是隐式传参,不能直接上手加const,因此,祖师爷(本贾尼)给出的方案是:
void Print() const
  • 即,直接在函数名后加const,就可以修饰 *this

如果成员函数不修改成员变量,就需要加上const防止成员被破坏

标签:函数,对象,C++,重载,int,year,Date,构造函数
From: https://blog.csdn.net/su1326669003/article/details/143588000

相关文章

  • 实验3 类和对象
    实验任务1:代码:1#pragmaonce23#include<iostream>4#include<string>56usingstd::string;7usingstd::cout;89//按钮类10classButton{11public:12Button(conststring&text);13stringget_label()const;14vo......
  • XMLHttpRequest以及Promise对象的使用
    AJAX原理通过[XHR]XMLHttpRequest对象来和服务器进行交互,axios库的底层也是通过XMLHttpRequest来和服务器进行交互,只是将实现细节进行了封装,让操作更加简洁可以用于某些只需和服务器进行少次交互的静态网站进行使用,减少代码的体积如何使用XMLHttpRequest创建对象配置请......
  • 实验3 类和对象_基础编程2
    任务1:#pragmaonce#include<iostream>#include<string>usingstd::string;usingstd::cout;//按钮类classButton{public:Button(conststring&text);stringget_label()const;voidclick();private:stringlabel;};......
  • 【C/C++】5.字节对齐和字节填充
    字节对齐(alignment)和字节填充(padding)是优化内存访问效率和确保数据结构正确存储的重要机制。了解字节对齐和填充的原理可以帮助我们更好地设计数据结构,并且减少因不合理的内存布局引起的性能问题或程序错误。1.字节对齐(Alignment)字节对齐是指在内存中存储数据时,将数据......
  • c++实验三
    task1:代码:button.hpp:1#pragmaonce23#include<iostream>4#include<string>56usingstd::string;7usingstd::cout;89//按钮类10classButton{11public:12Button(conststring&text);13stringget_label()......
  • 实验3 c++
    任务一:button.hpp:#pragmaonce#include"button.hpp"#include<vector>#include<iostream>usingstd::vector;usingstd::cout;usingstd::endl;//窗口类classWindow{public: Window(conststring&win_title); voiddisplay()const......
  • 实验3 类和对象 基础编程2
    实验任务1:源代码button.hpp:点击查看代码1#pragmaonce23#include<iostream>4#include<string>56usingstd::string;7usingstd::cout;89//按钮类10classButton{11public:12Button(conststring&text);13stringget_label(......
  • 实验3 类和对象_基础编程2
    任务1源程序:button.hpp1#pragmaonce23#include<iostream>4#include<string>56usingstd::string;7usingstd::cout;89//按钮类10classButton{11public:12Button(conststring&text);13stringget_label()cons......
  • 实验3 类和对象_基础编程2
    任务一task1.cppbutton.hpp#pragmaonce#include<iostream>#include<string>usingstd::string;usingstd::cout;//按钮类classButton{public:Button(conststring&text);stringget_label()const;voidclick();private:string......
  • 实验3 类和对象_基础编程2
    实验任务1button.cpp源码#pragmaonce#include<iostream>#include<string>usingstd::string;usingstd::cout;//按钮类classButton{public:Button(conststring&text);stringget_label()const;voidclick();private:string......