一、类的引入
C++中的结构体是一种类,在其中不仅可以定义变量,还可以定义函数。
struct person
{
//成员函数
void Print()
{
cout << name << '-' << sex << '-' << age << endl;
}
//成员变量
char name[100];
char sex[10];
int age;
};
int main()
{
class person one;
cout << "name:";
cin >> one.name;
cout << "sex:";
cin >> one.sex;
cout << "age:";
cin >> one.age;
one.Print();
return 0;
}
值得注意的是C++中结构体成员函数定义和声明分离的写法
//头文件(.h)
#pragma once
#include<iostream>
using namespace std;
struct person
{
void Print();//成员函数的声明在头文件中写
char name[100];
char sex[10];
int age;
};
//函数文件(.cpp)
#include"标头.h"
void person:: Print()//成员函数的定义要指明是在哪一个结构体的类中
{
cout << name << '-' << sex << '-' << age << endl;
}
//测试文件(.cpp)
#include"标头.h"
int main()
{
class person one;
cout << "name:";
cin >> one.name;
cout << "sex:";
cin >> one.sex;
cout << "age:";
cin >> one.age;
one.Print();
return 0;
}
二、类访问限定符与封装
(1)类访问限定符
1.类别
public(公有)、protected(保护)、private(私有);目前我们认为保护和私有是一样的
思考:C++中class和struct的区别是什么?
解答:C++需要兼容C语言,所以C++中struct可以当成结构体使用。另外C++中 struct还可以用来定义类。和类定义类是一样的,区别是结构(Struct)定义的类默认访问权限是public,类(Class)定义的类默认访问权限是private。注意:在继承和模板参数列表位置,结构和类也有区别,后序给大家介绍。
class person
{
public:
void Print()
{
cout << name << '-' << sex << '-' << age << endl;
}
private:
char name[100];
char sex[10];
int age;
};
(2)封装
面向对象三大特性:封装,继承,多态
封装:将数据和操作数据的方法有机结合,隐藏对象属性,仅开放接口来实现对对象的操作,它的本质是一种管理,让用户更方便地使用类。
三、类的实例化
(1)概念
用类来创建对象的过程叫做类的实例化
1.类是对对象结构的描述,是图纸,定义一个类,内存并不会为此分配存储空间
2.一个类可以实例化出多个对象,占用存储空间,存储成员变量
3.类的实例化就像根据图纸(类的定义)造房子(类的实例化),只有造出房子后才能使用
class stack
{
public:
int a;
double b;
char c;
};//这样定义了一个类,内存不会为其分配存储空间
int main()
{
stack m;//类的实例化是在main函数中进行的
m.a = 3;
stack::a = 3;//这种写法是错误的,就像只有图纸,没有房子一样
}
(2)类的存储大小计算
1.类的存储大小计算不算函数,只算类中的变量和嵌套的类,当然要遵循
结构体对齐原则:
a.第一个成员在与结构体偏移量为0的地址处。
b.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
c.对齐数=编译器默认的一个对齐数 与 该成员大小的较小值。
d.VS中默认的对产数为8
e.结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
f.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大 小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
class A1
{
public:
void f1();
private:
int _a;
char _ch;
};
思考:结构体为什么要对齐?
计算机中的内存读取由地址总线决定,一般是16位,32位,64位(分别对应2,4,8个字节),结构体对齐可以有效减少内存读取的次数
四、this指针
(1)引入
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1, d2;
d1.Init(2022, 1, 11);
d2.Init(2022, 1, 12);
d1.Print();// call Print(ox21311111)
d2.Print();// call Print(ox21311111)
return 0;
}
思考:d1和d2对于print函数的调用明明call的地址都是一样的,为啥打印的东西都不同,
这是因为this指针的调控。
(2)this指针的特性
1.this指针的类型是:*const
2.this指针的本质是成员函数的形参,当对象调用成员函数时,将对象的地址作为实参传递给this形参,对象中不储存this指针
//Print函数实际上是这样的
void Print(Date* const this)
{
cout << _year << "-" << _month << "-" << _day << endl;
}
//this接收的是对象的地址(即d1与d2的地址)
这是编译器的暗箱操作
3.this指针只能在成员函数内部使用
//像这样
void Print()
{
cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
}
4.this指针是成员函数隐含的指针形参,一般由编译器通过ecx寄存器自动传递,不需要用户传递
面试题:
1.this指针存在哪里?
this指针是成员函数的参数,当然存在成员函数的栈区
2.this指针可以为空吗?
左边可以正常运行 右边会报错
&p实际是就是this指针,调用形参是空指针的成员函数,只要成员函数内部没有涉及到对成员变量的访问,就没有什么问题。
五、构造函数
(1)概念
构造函数是特殊的成员函数,名字与类名相同,创建类型对象时由编译器自动调用,负责对对象进行初始化(而不是创建对象),在对象的生命周期中只会调用一次
(2)特征
1.函数名与类名相同
2.没有返回值(定义时也不用写void)
3.对象实例化时由编译器自动调用
4.构造函数可以重载(以实现不同的初始化方式)
class Date
{
public:
//1.无参数构造
Date()
{
}
//2.带参数构造
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//上两个函数的定义也说明构造函数可以重载
void Print()
{
cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//对象+参数列表,调用构造函数
Date d1;//调用无参构造
Date(2025, 2, 1);//调用参数构造
Date();//这种写法是错误的,与函数的声明冲突
return 0;
}
5.如果类中没有写构造函数的定义(显式定义函数),C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义了,编译器就不再生成
class Date
{
public:
/*Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}*/
//把构造函数屏蔽掉,依然正常运行,证明编译器生成了一个无参的默认构造函数
//把构造函数取消屏蔽后,因为main函数中是无参调用,与构造函数不匹配,编译器报错,证明显式定义后编译器不在生成无参的默认构造函数
void Print()
{
cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
return 0;
}
6.如果使用无参的默认构造函数初始化类,一般来说实际上其中的成员变量是随机值(可以说没有进行过初始化)
这是因为:
首先我们了解变量的一种分类
a、内置类型/基本类型、语言本省定义的基础类型int/char/double/指针等等
b、自定义、用struct/class等等定义的类型
编译器对内置类型是不会进行处理的,而对自定义类型会按调用它的默认构造,我们不写,编译器默认生成构造函数,内置类型不做处理,自定义类型会去调用他的默认构造(写在自定义类中的这个类的不含参的构造函数),有些编译器也会处理但是那是个性化行为不是所有编译器都会处理
SO:
a、一般情况下,有内置类型成员,就需要自己写构造函数不能用编译器自己生成的。
b、全部都是自定义类型成员,可以考虑让编译器自己生成
7.C++11后,内置类型成员变量在类中声明时可以给默认值,由此避免对象成员变量初始化时的随机值
class Date
{
public:
void Print()
{
cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
}
private:
int _year = 2025;
int _month = 2;
int _day = 1;
};
8.无参的构造函数,全缺省的构造函数,以及编译器生成的构造函数都叫做默认构造函数(不传参就调用的构造函数),并且默认构造函数只能有一个
注意以下这种情况:
注意:让编译器自己生成的构造函数bug很多,一般情况下构造函数还是自己写好,当内置成员变量全都有符合要求的缺省值或者成员变量全是自定义类型,且已有默认构造(全缺省,无参的自定义类型的构造函数)时才考虑让编译器自己生成构造函数
还有:如果成员变量中的自定义类型的类中写的构造函数是下面那个,这样也是会报错的,因为下方那个不是默认构造函数
六、析构函数
(1)概念
对象在销毁时会自动调用析构函数,完成对象中资源的清理工作
(2)特性
1.析构函数是在类名前加上字符~
class stack
{
public:
stack(int capacity = 4)
{
_top = 0;
int* tmp = (int*)malloc(sizeof(int) * capacity);
if (tmp == NULL)
{
perror("maaloc failure");
return ;
}
_arr = tmp;
_capacity = capacity;
}
//析构函数写法
~stack()
{
_top = 0;
_capacity = 0;
free(_arr);
_arr = nullptr;
}
private:
int _top;
int _capacity;
int* _arr;
};
2.析构函数无参无返回值(不能重载)
3.一个类只有一个析构函数,若没有显式定义,系统会自动生成默认析构函数
4.对象生命周期结束时,编译器会自动调用析构函数
5.关于编译器自己生成的析构函数:对内置类型不做处理,对于自定义类型会调用它的默认析构函数(写在这个自定义类型中的析构函数)
class Time
{
public:
//time这个类的默认析构函数
~Time()
{
cout << "~Time" << endl;
}
private:
int _hour;
int _minitue;
int _second;
};
class Date
{
public:
Date()
{
_year = 0;
_month = 0;
_day = 0;
}
private:
//内置类型
int _year;
int _month;
int _day;
//自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
//程序运行结束,屏幕打印~Time,表示Time析构函数已经运行
//具体过程:main中调用Date类,对d实例化时创建了d._t,就去调用Time类,对_t实例化,
//main函数运行到return时就要调用析构函数清理资源,内置类型不做处理,只对自定义类型_t进行处理,
//调用其析构函数发现在Date类中没有它的析构函数,
//于是到Time类在调用它的的析构函数,完成对自定义类型的销毁
6.讨论一下什么时候要自己写析构函数,什么时候用编译器创造的析构函数
若类中没有申请资源(如Date)用编译器创造的析构函数即可;
若类中申请了资源(如Stack)需要自己写析构函数释放资源;
七、拷贝构造函数
(1)概念
只有单个形参,该形参是该类型的引用(常用const修饰),在将这个类已存在变量内容拷贝到这个类的另一个变量时由编译器自动调用
class Date
{
public:
//Date的含参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//Date的拷贝构造函数
Date(const Date& d)//注意它的形参
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2025, 6, 28);
Date d2(d1);//调用拷贝构造函数将d1的内容拷贝到d2
return 0;
}
(2)特性
1.拷贝构造函数是构造函数的一个重载形式
2.拷贝构造函数只有一个形参,且是对这个类的对象的引用,使用传值方式调用会引发编译器报错(因为引发了类的无穷调用)
解释:在C++中函数的调用有如下规则:调用一个函数要先传参,内置类型的传参就是形参直接拷贝实参,而自定义类型的拷贝要调用拷贝构造函数进行拷贝
在(1)概念所举的例子如果将拷贝构造函数写成以下这种形式
Date(Date d)//不使用引用,传值调用
{
_year = d._year;
_month = d._month;
_day = d._day;
}
因为Date是一个自定义类型,在main中对d2进行实例化时跳转到Date的拷贝构造函数(d2(d1)),d作为形参要拷贝实参d1,对d进行实例化时跳转到Date的拷贝构造函数(d(d1)),这时d作为形参要拷贝实参d1.....如此循环,编译器会报错
解决方法有二:
a.使用指针(指针是内置类型d直接拷贝d1的地址,通过对d1地址的解引用进行d1拷贝到d2)
Date(Date* d)//使用指针,传址调用
{
_year = d->_year;
_month = d->_month;
_day = d->_day;
}
b.使用引用(避免形参对实参的拷贝,直接使用实参操作,避免进入循环)d是d1,this是d2(赋值顺序不要搞反)
Date(Date& d)//使用引用,避免形参对实参的拷贝,避免进入循环
{
_year = d->_year;
_month = d->_month;
_day = d->_day;
}
下面讨论为什么形参要用const修饰
Date(Date& d)
{
/*year = d.year;
month = d._month;
day = d._day;
d.year = year; */
//屏蔽部分的写法是对的
d._year = _year;
d._month = _month;;
d._day = _day;
//这个写法是错的,相当于把未初始化的d2的内容拷贝给d1,拷反了
}
这是难以发现的错误写法,形参加上const可以有效避免这种情况
3.若没有类中拷贝构造函数,编译器会自动生成默认的拷贝构造函数,默认的拷贝构造函数对象按序按字节进行拷贝,这种拷贝叫浅拷贝或者值拷贝
默认构造函数拷贝原则:对于内置类型采用浅拷贝,对于自定义类型,调用该类的拷贝构造函数
接下来看看这三个例子,看看是否要自己写拷贝构造函数
a.成员变量只含有不是指针的内置类型
class Date
{
public:
//Date的含参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//成员变量全是不是指针的内置类型
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2025, 6, 28);
Date d2(d1);
return 0;
}
这种情况下使用编译器生成的默认拷贝构造函数即可
b.成员变量是含有指针的内置类型
class stack
{
public:
//构造函数
stack(int capacity = 4)
{
_top = 0;
int* tmp = (int*)malloc(sizeof(int) * capacity);
if (tmp == NULL)
{
perror("maaloc failure");
return ;
}
_arr = tmp;
_capacity = capacity;
}
//析构函数
~stack()
{
_top = 0;
_capacity = 0;
free(_arr);
_arr = nullptr;
}
//内置类型中含有指针
private:
int _top;
int _capacity;
int* _arr;
};
int main()
{
stack d1;
stack d2(d1);
return 0;
}
可以看到对于_top和_capacity两个非指针内置类型拷贝完全符合,但对于指针_arr的拷贝d1和d2的_arr指向的空间完全相同,这会导致以下两个问题:
对d1和d2析构时会导致_arr指向的空间被释放两次,导致程序奔溃
通过_arr对d1和d2的操作会同时作用于d1和d2两个变量
改进:在stack中写一个这样的拷贝构造函数处理指针内置类型_a
stack(const stack& st)
{
_arr = (int*)malloc(sizeof(int) * st._capacity);
if (nullptr == _arr)
{
perror("malloc申请空间失败");
return;
}
memcpy(_arr, st._arr, sizeof(int) * st._top);
_capacity = 0;
}
c.成员变量是自定义类型
class Time
{
public:
//构造函数
Time()
{
_hour = 0;
_minitue = 0;
_second = 0;
}
//拷贝构造函数
Time(Time& t)
{
_hour = t._hour;
_minitue = t._minitue;
_second = t._second;
}
//析构函数
~Time()
{
cout << "~Time" << endl;
}
private:
int _hour;
int _minitue;
int _second;
};
class Date
{
public:
//构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//没有Date的拷贝构造函数
private:
//内置类型
int _year;
int _month;
int _day;
//自定义类型
Time _t;
};
int main()
{
Date d1(2025, 6, 28);
Date d2(d1);
return 0;
}
通过调试,当程序运行到d1拷贝给d2 (Date d2(d1))这句代码时,我们讨论d1._t这个成员变量要进行拷贝时,会跳到Time这个类中调用它的拷贝构造函数对d1._t进行拷贝。所以,对于自定义类型的成员变量,在该成员变量所在的类中没有这个变量的拷贝构造函数时(Date这个类中没有拷贝构造函数),编译器默认生成的拷贝构造函数会调用这个类(Time)的拷贝构造函数(Ti2me中的拷贝构造函数)对d2._t进行拷贝。
总结:当成员变量是不是指针的内置类型时,不用自己写拷贝构造函数,使用编译器自己生成的即可;当成员变量有指针和自定义类型时,要自己写拷贝构造函数(指针在指针作为成员变量所在的这个类中写;自定义类型在它自己的那个类中写)
八、赋值运算符重载
(1)运算符重载
1.引入
以下是实现d1==d2的运算符重载的一种方法:
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
public:
int _year;
int _month;
int _day;
};
bool operator==(Date& x, Date& y)
{
return (x._year == y._year) && (x._month == y._month) && (x._day == y._day);
}
int main()
{
Date d1(2025, 6, 28);
Date d2(2025, 7, 28);
cout << (d1 == d2) << endl;
return 0;
}
很容易发现这种写法有缺点:成员变量必须是public才能被operator==函数访问
若要让成员变量保持pravite,可以把operator==函数写在Date类型内部
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//这里d1是*this,d2是x
//bool operator==(Date* this,Date& x)
bool operator==(Date& x)
{
return (_year == x._year) && (_month == x._month) && (_day == x._day);
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2025, 6, 28);
Date d2(2025, 7, 28);
cout << (d1 == d2) << endl;
return 0;
}
编译器进行的转换是这样的:
汇编角度
2.概念与特性
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数
函数名字:operator操作符
注意:
a.不能使用无意义的操作符进行运算符重载,如:operator@
b.运算符的操作数是几个,运算符重载函数的参数就有几个(定义在类外,操作数几个,函数参数就有几个;定义在类内,函数参数是操作数-1(因为其中一个是隐藏的this,不显示))
c. .* :: sizeof ?:(三目) . 这五个运算符不能重载
d.内置类型进行运算符操作是使用底层指令,自定义类型是调用运算符重载函数
这是从汇编角度:
(2)赋值运算符重载
1.格式:
a.参数类型:const 类名&,传递引用可以提高效率,const是为了保证用于赋值的参数(d1的拷贝)不被改变(*this(d2的拷贝)是要被改变的,因为要被赋值)
b.返回值类型:类名&,返回引用可以提高效率(this虽然是d2地址的拷贝,是函数参数,存在它的栈区,函数结束即消失,但是*this(d2本身)是不会消失的)
c.注意检查自己给自己赋值的情况
d.返回*this,以符合连续赋值的作用
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
Date& operator=(const Date& x)//引用返回提高效率
{
if (this != &x)//排除自己赋值给自己的情况
{
_year = x._year;
_day = x._day;
_month = x._month;
}
return *this;//返回*this以满足连续赋值
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2025, 6, 28);
Date d2(2025, 7, 28);
Date d3(2026, 6, 29);
d2 = d1;//依次赋值
d1.Print();
d2.Print();
cout << endl;
d1 = d2 = d3;//连续赋值
d1.Print();
d2.Print();
d3.Print();
return 0;
}
注意:要区别拷贝构造函数和赋值运算符重载函数
2.赋值运算符函数只能定义在类中,不能定义在类外(但可以在类中声明,在类外定义)
原因:当你在main函数是写了自定义类型的运算符操作代码,如上例的d1=d2,编译器在执行到这行代码时,会去类中寻找赋值运算符重载函数,如果找不到,就会在类中生成一个默认赋值运算符重载函数。如果你在类外写了一个赋值运算符重载函数,就会和这个类中编译器生成的默认赋值运算符重载函数相冲突,导致报错
3.当用户没有显式实现时,编译器会生成一个默认赋值运算符重载函数,以浅拷贝的方式进行赋值
注意:这里编译器生成的 默认赋值运算符重载函数 与 默认拷贝构造函数 是类似的,对内置类型不做处理,对自定义类型调用它的 默认赋值运算符重载函数 易错点也与拷贝构造函数相同,当类中涉及到资源申请,就有自己在类中写 默认赋值运算符重载函数 ,当类中没有涉及到资源申请,就使用编译器生成的 默认赋值运算符重载函数 即可
(3)前置++与后置++
class Num
{
public:
Num(int number)
{
_number = number;
}
//这里为了防止函数重名导致编译错误,只好使二者参数不同构成重载
//增加这个int参数不是为了接收具体的值,仅仅是占位,跟前置++构成重载
Num operator++(int)//后置++有参数,传值返回
{
Num tmp = *this;
(*this)._number++;
return tmp;
}
Num& operator++()//前置++没有参数,引用返回
{
(*this)._number++;
return *this;
}
private:
int _number;
};
int main()
{
Num d1(30);
//返回++前的值
d1++;// d1.operator++(0)
//返回++后的值
++d1;// d1.operator++()
return 0;
}
(4)<<与>>
1.为什么<<与>>有自动识别参数类型的功能?
2.关于iostream
可以看到:cin是istream类型的对象,cout是ostream类型的对象
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void operator<<(ostream& out)//注意理解这里的参数
{
out << _year << " " << _month << " " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2025, 6, 28);
//cout << d1;//这样写会报错
d1 << cout;//这样写可以通过,编译器调用:d1.operator<<(cout)
return 0;
}
这种写法虽然成功打印,但是写起来怪怪的,所以我们一般认为<<与>>不能写成成员函数,因为Date对象默认占用第一个参数(作为*this),就是做了左操作数,右操作数才是cout
改进方法:把operator<<写在类外
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
public://注意成员变量要是public才能被类外函数访问使用
int _year;
int _month;
int _day;
};
void operator<<(ostream& out,const Date d)//注意理解这里的参数
{
out << d._year << " " << d._month << " " << d._day << endl;
}
int main()
{
Date d1(2025, 6, 28);
cout << d1;//operator<<(cout, d1);
return 0;
}
这种写法,它的成员变量只能是public,封装不能够保持,所以我们引入 友元函数
class Date
{
friend void operator<<(ostream& out, const Date d);//友元函数在类中的声明
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
void operator<<(ostream& out,const Date d)
{
out << d._year << " " << d._month << " " << d._day << endl;
}
int main()
{
Date d1(2025, 6, 28);
cout << d1;//operator<<(cout, d1);
return 0;
}
还有一种方法是专门在类中写一个获取_year _month _day的函数
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//定义得到年月日的函数
int GetYear()
{
return _year;
}
int GetMonth()
{
return _month;
}
int GetDay()
{
return _day;
}
private:
int _year;
int _month;
int _day;
};
void operator<<(ostream& out, Date d)
{
//通过调用类内函数来获取private的成员变量
out << d.GetYear() << " " << d.GetMonth() << " " << d.GetDay() << endl;
}
int main()
{
Date d1(2025, 6, 28);
cout << d1;
return 0;
}
如果要实现:cout<<d1<<d2<<d3 这样的连续打印呢?
应该是先打印d1,然后d2,然后d3
class Date
{
friend ostream& operator<<(ostream& out, const Date d);//友元函数在类中的声明
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private://注意成员变量要是public才能被类外函数访问使用
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out,const Date d)//注意这里的返回类型
{
out << d._year << " " << d._month << " " << d._day << endl;
return out;//注意这里的返回值
}
int main()
{
Date d1(2025, 6, 28);
Date d2(2024, 6, 28);
Date d3(2023, 6, 28);
cout << d1 << endl;
cout << d1 << d2 << d3;
return 0;
}
下面类别实现以下>>的函数运算符重载
class Date
{
friend ostream& operator<<(ostream& out, const Date d);//友元函数在类中的声明
friend istream& operator>>(istream& in, Date& d);//友元函数在类中的声明
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, const Date d)//注意这里的返回类型
{
out << d._year << " " << d._month << " " << d._day << endl;
return out;//注意这里的返回值
}
istream& operator>>(istream& in, Date& d)//注意这里的返回类型
{
int year = 0, month = 0, day = 0;
//定义三个变量,用于接收键盘上输入的值
in >> year >> month >> day;
//将键盘上输入的值赋值给d
d._year = year;
d._month = month;
d._day = day;
return in;//注意这里的返回值
}
int main()
{
Date d1(2025, 6, 28);
Date d2(2024, 6, 28);
Date d3(2023, 6, 28);
cin >> d1 >> d2 >> d3;
cout << endl;
cout << d1 << d2 << d3;
return 0;
}
(5)const成员调用函数
下面解决这个问题
原因分析:
d2.Print函数的调用涉及到了权限的放大(从常变量到常量)
这样改:
只需要在改为void Print()const
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()const//这里写const实际上是把Date* this指针改为const Date* this
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2025, 6, 28);
d1.Print();//可以调用
const Date d2(2024, 6, 28);
d2.Print();//可以调用
return 0;
}
对于const修饰成员函数有以下结论
(6)&运算符重载函数
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
Date* operator&()
{
return this;
}
const Date* operator&()const//注意处理const成员传递时的权限变大问题
{
return this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2025, 6, 28);
const Date d2(2026, 6, 28);
cout << &d1 << endl;
cout << &d2 << endl;
return 0;
}
这两个运算符重载函数没有什么价值,让编译器自己生成就可以了
九、再谈构造函数
(1)初始化列表
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
这是一个简单的Date类,类中有一个Date的含参构造函数,通过赋值的方式给成员变量赋初值,这叫做构造函数体赋值,构造函数体中的语句只能将其称为赋初值,而不能叫做初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
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.类中包含以下成员,必须在初始化列表初始化
a.引用成员变量
b.const成员变量
c.自定义类型成员(该自定义类型没有默认构造时)
class A
{
public:
A(int a)//这个是含参构造函数,不是默认构造函数
:_a(a)
{
}
private:
int _a;
};
class B
{
public:
B(int a,int b,int c)
:_a(a)//自定义类型在初始化列表给值构造,是对应了这个类在类中带参构造函数的使用
,_ret(b)
,_n(c)//这里B的形参c存在栈区,但这个函数结束时,c会销毁,导致_n的引用对象消失,所以这样的写法是有风险的
{
}
private:
A _a;//没有显示默认构造函数
const int _ret;//const成员变量
int& _n;//引用成员变量
};
int main()
{
B d1(1,3,5);
}
下面看一个易错点
另外,当自定义类型有默认构造时,也可以在初始化列表中初始化,默认构造就像缺省参数一样,可用可不用。且自定义类型在初始化列表给值构造,是对应了这个类在类中带参构造函数的使用
3.尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
4.成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
class A
{
public:
A(int a)
:_a1(a)
,_a2(_a1)
{
}
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2;
int _a1;
};
int main()
{
A aa(1);
aa.Print();
return 0;
}
通过理解这个例子,就可以理解上面那句话了,所以我们写代码时,一般让声明顺序和初始化顺序一致
十、自定义类型的隐式类型转换
发生隐式类型转换是中间会生成一个临时变量:
对于下方:把i拷贝给临时变量,再将临时变量转换为double类型拷贝给d
对于上方:用2去调用A的构造函数,生成一个A类型的临时对象,再用A类型的拷贝构造,把临时对象拷贝给aa2,一共调用一次构造和一次拷贝构造,但vs编译器会直接优化为一次构造
十一、explicit关键字
从上面的例子可以看出,以下几种构造函数有隐式类型转换的作用:
1.构造函数只有一个参数
2.构造函数有多个参数,除了第一个其他都有缺省值
3.全缺省构造函数
而使用explicit修饰构造函数可以禁止隐式类型转换
class A
{
public:
explicit A(int a)
:_a(a)
{
}
private:
int _a;
};
int main()
{
A a1(1);
A a2 = 2;//这一行会报错
return 0;
}
十二、static成员
static可以修饰成员变量和成员函数,分别叫做静态成员变量和静态成员函数,静态成员变量只能在类外初始化。
面试题:实现一个类计算程序中创建了多少个变量
class A
{
public:
A(int a)
:_a(a)
{
cout << "A(int a)" << endl;
count++;
}
~A()
{
cout << "~A()" << endl;
}
static int getcount()//返回的是静态变量要写static修饰函数
{
return count;
}
private:
int _a;
static int count;//类中的静态成员变量的声明,只声明一次
};
int A::count = 0;//静态成员变量要在类外定义,且不用写static
int main()
{
A a1(1);
A a2 = 2;
//下面这两行会报错,因为静态成员变量count受private限制
cout << A::count << endl;
cout << a1.count() << endl;
//考虑在类中写一个publlic的函数来获取count
cout << A::getcount() << endl;
cout << a1.getcount() << endl;
return 0;
}
特性:
1.成员变量是属于类的对象,储存在对象里面,静态成员变量属于类,是这个类的所有对象共同使用的,存放在静态区
2.静态成员变量在类中声明,写static,在类外初始化,不写static,不能用初始化列表初始化
3.类静态成员用 类名::静态成员 或者 对象.静态成员访问
4.静态成员函数没有this指针,不能访问任何非静态成员
class A
{
public:
A(int a)
:_a(a)
{
cout << "A(int a)" << endl;
count++;
}
~A()
{
cout << "~A()" << endl;
}
static int getcount()//返回的是静态变量要写static修饰函数
{
_a++;//这一行会报错,静态成员函数没有this指针,无法接收对象的地址,进而无法访问成员变量
return count;
}
private:
int _a;
static int count;//类中的静态成员变量的声明
};
5.静态成员也受访问限定符的限制
注意:
class A
{
public:
A(int a)
:_a(a)
{
cout << "A(int a)" << endl;
count++;
}
~A()
{
cout << "~A()" << endl;
}
static int getcount()
{
return count;
}
private:
int _a;
static int count = 0;//静态变量是无法给缺省值的,因为没有构造函数去使用缺省值
};
6.静态成员函数不可以调用非静态成员函数,非静态成员函数可以调用静态成员函数
十三、友元
友元是一种破坏封装的方式,一般不建议大量使用
(1)友元函数
特性
1.友元函数可以访问类的private和protacted成员,但它不是类的成员函数
2.友元函数不能用const修饰
3.友元函数可以在类定义的任何地方声明,不受类访问限定符的限制
4.一个函数可以是多个类的友元函数
5.友元函数的调用原理和一般函数相同
class Date
{
friend ostream& operator<<(ostream& out, const Date d);//友元函数在类中的声明
friend istream& operator>>(istream& in, Date& d);//友元函数在类中的声明
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, const Date d)//注意这里的返回类型
{
out << d._year << " " << d._month << " " << d._day << endl;
return out;//注意这里的返回值
}
istream& operator>>(istream& in, Date& d)//注意这里的返回类型
{
int year = 0, month = 0, day = 0;
//定义三个变量,用于接收键盘上输入的值
in >> year >> month >> day;
//将键盘上输入的值赋值给d
d._year = year;
d._month = month;
d._day = day;
return in;//注意这里的返回值
}
int main()
{
Date d1(2025, 6, 28);
Date d2(2024, 6, 28);
Date d3(2023, 6, 28);
cin >> d1 >> d2 >> d3;
cout << endl;
cout << d1 << d2 << d3;
return 0;
}
(2)友元类
友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类的非public成员
特性
1.友元关系是单向的,没有交换性
2.友元关系不能传递,A是B的友元,B是C的友元,不能的得出A是C的友元
3.友元关系不能传递
十四、内部类
一个类定义在另一个类的内部就叫内部类(B是A的内部类)
class A
{
public:
private:
class B
{
public:
private:
int _a;
};
int _b;
};
特性:
1.内部类是外部类的友元,外部类没有优越访问内部类的权限
2.内部类受访问限定符的限定
3.sizeof(外部类)=外部类,内部类相当于一种声明,不占用空间大小。
class A
{
public:
A(int a)
:_a(a)
{
}
class B
{
public:
B(int b)
:_b(b)
{
}
private:
int _b;
};
int _a;
};
int main()
{
cout << sizeof(A) << endl;//4
cout << sizeof(B) << endl;//报错:未定义
A a(1);
A::B b(2);
cout << sizeof(a) << endl;//4
cout << sizeof(b) << endl;//4
return 0;
}
十五、匿名对象
class A
{
public:
A(int a)
:_a(a)
{
}
int get()
{
return _a;
}
private:
int _a;
};
int main()
{
A a1(1);//正常对象
A(2);//匿名对象
cout << A(2).get() << endl;//匿名对象的使用
}
class A
{
public:
A(int a)
:_a(a)
{
}
int get()
{
return _a;
}
private:
int _a;
};
int main()
{
A a1(1);//正常对象,生命周期是main函数
A(2);//匿名对象,生命周期只是这一行
cout << A(2).get() << endl;//匿名对象的使用
}
class A
{
public:
A(int a)
:_a(a)
{
}
private:
int _a;
};
int main()
{
A a1(1);//正常对象
A(2);//匿名对象
A b = A(3);//构造A(3),拷贝构造b
A& c = a1;//正确
A& c = A(3);//报错,匿名对象具有常性,权限放大
const A& c = A(3);//正确,权限平移,const引用可以把c(也就是匿名对象A(3)的生命周期延长到main函数
}