类和对象
类
- 通常情况下采用class定义(默认private)
- 由于C++向下兼容的特性,也可采用struct定义类(默认public)
class Func //默认都是私有,private,外部函数不能通过成员名直接访问
{
int _x;
int _arr[10];
};
public Func // 默认都是公共,public,外部函数能通过成员名直接访问
{
int _x;
int _arr[10];
};
①类的声明与定义
- 成员函数的类声明放在头文件(.h),成员函数定义放在源文件(.cpp)
- 注意,如果成员函数在类里面定义,将会被认为是定义内联函数
- 因此当成员函数在类里面定义时,不要声明与定义分离
②成员变量的规范化
- 查看以下代码的问题:
class Date
{
public:
void Init(int year)
{
year = year;
}
private:
int year;
};
这段代码中,Init函数中的参数是year,而成员变量也是year,会导致编译器不确定访问哪一个year,因此需要对成员变量的名称规范化
private:
int _year;
- 通常情况下在变量前加一个杠即可,具体看地方要求或是个人喜好
1. 成员变量为声明
- 类中的成员变量都是声明,没有实际开空间,需要类实例化对象去使用
类似图纸,建筑工程中的图纸就是成员变量,只是一个模板声明,实际建成的房屋就是类实例化对象,才能入住使用。
③计算类的大小
- 计算下面类的大小
class A
{
public:
void f1(){}
private:
int _a;
char _ch;
};
-
计算类的大小与结构体计算方法类似,先找到对齐数(比较默认对齐数和当前成员变量中最大的变量,取两个中的最小值,默认对齐数一般而言是8个字节)
-
计算类时,不看成员函数,只看成员变量
-
这里类的大小为4
- 当类中没有成员变量时:
类的大小为1。没有成员变量的类对象,需要1byte,是为了占位,表示对象存在
1. 对齐数的意义
- 假设:硬件在正常情况下,每次搜索的字节为4。并且修改上面的代码:
class A
{
public:
void f1(){}
private:
char _ch;
int _a;
};
某次访问类时,如果类没有按照对齐规则存储,而是:
如果按照这样的方式存储,那么为了读完_a的数据,CPU将会连续读取2次数据。对于_a而言,就是让CPU读了整整两次(对于_ch没什么影响)
某次访问类,如果按照对齐规则存储,则:
对于_a而言,只需要让CPU读取一次就能获得数据
- 因此内存对齐是通过多开了内存,花费更多的空间使得数据读取速度更快:
- 典型的空间换时间
也有办法让内存不对齐,一般在嵌入式这种对内存有限制的环境下可能会用到。
2. 错题展示
④成员函数的位置以及传参细节
1. 成员函数的位置
- 当调用成员函数时,实际上实在应该公共区域去调用而非类内部,这样做的好处是:
- 当有有个类实例化对象要调用成员函数时,就不需要到类里面重新展开函数使用。如果有一百个类的实例化对象调用了成员函数,那么就要展开一百个这样的成员函数,非常麻烦。因此我们把成员函数放在一个公共区域,每次调用时就直接call公共区域的成员函数,用完了后就放在原地等待下一个调用…
- 这也解释了为什么计算类大小时不考虑成员函数,因为成员函数都不在类里面。
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指针
- this指针是隐含的传参,是调用函数的成员的指针
- 只能生成于非静态区的成员函数对象
- 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. 构造函数
概念
- 构造函数主要是完成初始化的工作,在函数创建时就自动执行
特性
- 函数名与类名相同
- 不需要返回值
- 对象实例化时自动调用对应的构造函数
- 构造函数可以重载
- 如果类中没有显式定义构造函数,则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; }
输出结果:
默认构造函数(支持不传参的构造函数)
- 不传参就能使用的构造函数即默认构造函数
- 无参构造函数
- 全缺省构造函数
- 当没有显示定义时,编译器自动生成的构造函数
- 如果我们没有创建构造函数,编译器会默认创建构造函数,但是一般不用编译器自动生成的构造函数,因为他不会对内置类型初始化(对自定义类型会去调用它的默认构造函数)
部分编译器会个性化处理内置类型(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; }
是否需要创建构造函数
- 需要创建构造函数:
一般情况下,需要自己写构造函数
- 不需要创建构造函数:
内置类型成员都有缺省值,且初始化符合我们的要求
成员变量全是自定义类型,且这些类型都定义了默认构造
2. 析构函数
概念
- 对象在销毁时会自动调用析构函数。完成对象中资源的清理工作
特性
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值类型。
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构
函数不能重载 - 对象生命周期结束时,C++编译系统系统自动调用析构函数
是否需要创建析构函数
- 需要创建析构函数
一般情况下,有动态申请资源(malloc…),就需要显示写析构函数释放资源
2.不需要创建析构函数
没有动态申请的资源
需要释放资源的成员都是自定义类型
析构函数的触发条件以及变量的析构顺序
当构造类变量后,如果都在局部内构造,那么析构顺序就是构造顺序的相反(因为栈是先进后出的原则)
例题:
2.
答案:
1.
2.
3.拷贝构造函数
概念
- 只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
特性
- 拷贝构造函数是构造函数的一个重载形式
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用
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++规定:
-
内置类型直接拷贝
-
自定义类型必须调用拷贝构造函数完成拷贝
编译器生成的拷贝构造函数与浅拷贝
- 若未显示定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝
内置类型成员完成值拷贝/浅拷贝
自定义类型成员会调用他的拷贝构造
浅拷贝带来的问题
如果成员变量中有动态申请的资源,如malloc后的数组_a:
问题:
- 析构两次,报错
- 一个修改会影响另一个
- 由浅拷贝引出的其他:
看以下的问题代码:
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. 注意
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义(例如(int)a + (int)b)
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐
藏的this - .* :: 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;
};
赋值重载
重载格式
- 参数类型:const T&,传递引用可以提高传参效率
- 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
- 检测是否自己给自己赋值
- 返回*this :要复合连续赋值的含义(例如,a = b = c(都是int类型),顺序是:c赋值给b,所以b = c的返回值就是b,再将这返回值赋值给a,所以a = b = c 的返回值就是a)
正确示范:
编译器默认生成的赋值重载
- 与拷贝构造一样,对内置类型不做处理,自定义类型调用自身的赋值重载
- 赋值运算符只能作为类的成员函数重载
赋值运算符在类中不显式实现时,编译器会生成一份默认的,此时用户在类外再将赋值运算符重载为全局的,就和编译器生成的默认赋值运算符冲突了,故赋值运算符只能重载成成员函数
不需要自己创建赋值重载的情况
- 只要求浅拷贝/值拷贝
- 成员类型全是自定义类型
拷贝构造与赋值重载的区别(具体情况看具体分析,赋值重载也可以成为拷贝构造)
- 拷贝构造函数的定义:用一个已经存在的对象初始化另一个对象
- 赋值重载的定义:已经存在的两个对象之间复制拷贝
运算符重载补充
重载后置++/后置- -的处理
- 赋值运算符重载只是特殊的运算符重载,在类内我们依然可以实现其他的运算符重载
- +=是重载后的运算符,表示对该类的天数加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