目录
一、取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义,编译器默认会生成
const stn::Date d1(2024, 12, 14);
cout<<&d1<<endl;
stn::Date d2(2024, 12, 14);
cout << &d2 << endl;
我们在类和对象中有说过,自定义类型不能直接用运算符,但是上实际它可以跑,这就是默认成员函数那如果我们要写的话就要实现两个版本
Date* operator&()
{
return this;
}
const Date* operator&()const
{
return this;
}
我们写了就不会再生成,日常自动生成的其实就可以了,如果我们不想被取到有效地址的话把上面两个this改成nullptr就好了
二、友元函数、友元类
内置类型为什么可以支持流插入和流提取呢其实都是库里面包含了的,库里面把它们都写成了函数重载
那我们可不可以对自定义类型来流插入流提取呢
void operator<<(ostream& out)
{
out << _year << "/" << _month << "/" << _day << endl;
}
这样子很快就实现好了但是我们编译的时候发现就会报错
stn::Date d1(2024, 12, 14);
cout << d1;
这里其实是因为参数不匹配的问题
void operator<<(Date*this,ostream& out);
我们cout是不是要匹配Date*,但是就匹配错了
换个顺序就好了
但是这个能跑,但是不符合我们的使用习惯和价值
所以它就不能做成成员函数因为this指针的位置不能调换
void operator<<(ostream& out, const Date & d)
{
out << d._year << "/" << d._month << "/" << d._day << endl;
}
为了方便这里直接删掉了命名空间
但是函数是私有的我们又不能访问
这引出了友元函数
friend void operator<<(ostream& out, const Date& d);
你是我的朋友当然就可以访问了
cout<<d1<<d2;
那我们要像这样子就是做不到的因为前面的是一个函数调用它的返回值是void
ostream& operator<<(ostream& out, const Date & d)
{
out << d._year << "/" << d._month << "/" << d._day << endl;
return out;
}
改成这样子就好了
你还可以用在类里面写成员函数通过间接的方式来获取它们的成员
那我们来写一下流提取
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
默认是空格和换行分割
说明:
友元是不宜多用的
友元函数可以访问类的私有和保护成员,但不是类的成员函数
友元函数不能用const修饰
友元函数可以在类定义的任何地方声明,不受类访问限定符限制
一个函数可以是多个类的友元函数
友元函数的调用与普通函数的调用原理相同
友元类
友元函数就是你一个人是我的朋友,你可以来随意访问我家
友元类是你一家都是我的朋友,你一家都可以来访问我家
说明:
友元关系是单向的,不具有交换性
就是A是B的朋友,A可以去B里面访问,但是B是不能去A里面访问的
友元关系不能传递
如果C是B的友元,B是A的友元,则不能说明C是A的友元
友元关系不能继承,这个得等到继承位置再说明
不到万不可以,不要定义友元
三、初始化列表
虽然我们的构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象成员变量的初始化,构造函数中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数可以多次赋值
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个成员变量后面跟一个放在括号中的初始值或表达式
为什么要引入初始化列表呢
class myquene
{
private :
Stack _pushst;
Stack _popst;
};
Stack(size_t n)
{
if (n == 0)
{
a = nullptr;
top = capacity = 0;
}
else
{
a = (int*)malloc(sizeof(int) * n);
if (a = nullptr)
{
perror("realloc fail");
exit(-1);//让程序以异常的方式退出,正常退出方式是0;
}
top = 0;
capacity = n;
}
}
像这种类stack就可以调用它的默认构造函数
但是我就是不给它写缺省呢,编译就通过不了了
接下来就是初始化列表
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{}
这样子混着用也是可以的
Date(int year, int month, int day)
:_year(year)
, _month(month)
{
_day = day;
}
初始化列表是每个成员定义的地方
不管你写不写,每个成员都要走初始化列表
Date d1(2024, 12, 14);
那我们这样把对象定义的时候整体不就定义了吗,每个成员就都开辟出来了,为什么还要引入初始化列表呢
比如说我在成员变量里面加一个
const int _x;
const成员的特点是必须在定义时初始化,所以定义的时候就要在初始化列表
Date(int year, int month, int day)
:_year(year)
, _month(month)
,_x(1)
{
_day = day;
}
那我成员变量定义了一个引用呢
int& _ref;
引用的特点也是在定义时初始化
Date(int year, int month, int day, int& i)
:_year(year)
, _month(month)
, _x(1)
, _ref(i)
{
_day = day;
}
void func()
{
_ref++;
}
Date d1(2024, 12, 14,n);
d1.func();
cout << n << endl;
我i是n的别名,ref又是i的别名,那么ref就是n的别名,所以引用成员也有价值,也是不能放在里面的,放在里面就是赋值了
那我们对自定义类型呢,没有默认构造,那么就得去调用它带参的构造,在它初始化的时候,在它定义的时候在它构造的时候
A _a;
_a(1)
所以我们就用初始化列表解决一下上面的问题
class myquene
{
public:
myquene(int capacity = 10)
:_pushst(capacity)
,_pushst(capacity)
{}
private:
Stack _pushst;
Stack _popst;
};
这样子就行了
每个成员都在初始化列表初始化,那么_day就不初始化了吗,是初始化了的,只是内置成员它不做处理罢了,所以给了随机值
我们将_a的构造函数给上缺省值
还是一样对内置成员不做处理,对自定义类型去调用它的默认构造
如果既有默认构造还有显示写的去调显示写的。
初始化列表其实就构造函数的一部分,编译器其实走的就是初始化列表的一个机制
C++11支持给缺省值在成员变量声明的时候,这个缺省值给初始化列表
如果初始化列表没有显示给值,就用这个缺省值,如果显示给值了,就不用这个缺省值
我们那个const成员变量在给完缺省值也可以在初始化列表里不写
注意:初始化列表是按声明的顺序给初始化,它不按定义的时候走
注意:1.每个成员在初始化列表中只能出现一次
2.类中包含一下成员必须放在初始化列表位置进行初始化
引用成员变量 const成员变量 自定义类型成员(且该类没有默认构造函数时)
3.尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会显示有初始化列表初始化
4.成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
这个就体现了函数体和初始化列表要混着用
Stack(int capacity = 10)
: _a((int*)malloc(capacity * sizeof(int)))
,_top(0)
,_capacity(capacity)
{
if (nullptr == _a)
{
perror("malloc申请空间失败");
exit(-1);
}
// 要求数组初始化一下
memset(_a, 0, sizeof(int) * capacity);
}
四、隐式类型转换
我们来看单参数构造的隐式类型转换这个很重要在以后会经常用到
class A
{
public:
A(int i)
:_a(i)
{
cout << "A(int i)" << endl;
}
A(const A& a)
:_a(a._a)
{
cout << "A(const A& a)" << endl;
}
private:
int _a;
};
A a1(1);
A a2 = 2;
就是下面这种,用2调用A构造函数生成一个临时对象再用这个对象去拷贝构造a2
编译器会再优化,优化为用2直接构造
拷贝构造也是一种特殊的构造也可以使用初始化列表,我们来验证一下
A a1(1);
SeqList st;
st.PushBack(a1);
st.PushBack(1);
上面那三行代码我直接用隐式类型转换一行代码就能完成
如果我不想让这个隐式类型转换发生怎么办
像这样加上这个explicit关键字就好了
explicit A(int i)
:_a(i)
{
cout << "A(int i)" << endl;
}
那么多参数的呢,C++98是不支持的,但是C++11就支持了
class B
{
public:
B(int b1, int b2)
:_b1(b1)
,_b2(b2)
{
cout << "B(int b1, int b2)" << endl;
}
private:
int _b1;
int _b2;
};
那么多参数我们怎么给值呢
B bb1 = 1, 2;
这样子显然是不行的
B bb1(1, 1);
B bb2 = { 1,2 };
const B& ref2 = { 3,3 };
这样就行了
C++还允许定义匿名对象
有名对象 特点:生命周期再当前局部域
A a1(6);
匿名对象 特点 声明周期只在这一行
A(7);
我们上面隐式类型转换被禁了就可以用这种情况,当然不止这一种场景
st.PushBack(A(8));
我们用这个对象只用这一次
这里很多人可能很难理解我给个生活中的例子来说明
单参数构造函数:
咖啡店提供了一种“特大杯”咖啡,只需要告诉店员你想要多大杯(即一个参数),店员就会默认给你一杯最大的咖啡。
类比: 在编程中,一个类(比如“咖啡”类)可能有一个构造函数,只需要传入一个参数(杯的大小),这个构造函数就会根据这个参数创建一个完整的咖啡对象。
隐式类型转换:
当你直接告诉店员“我要一杯特大杯”,店员就明白你的意思了,而不需要你详细说明你想要哪种豆子、多少糖等等。
类比: 在编程中,当我们创建一个咖啡对象时,可以直接传入一个数字表示杯的大小,编译器会自动将这个数字转换为“咖啡”类的对象。
五、static成员
int n = 0;
int m= 0;
class A
{
public:
A()
{
++n;
++m;
}
A(const A& t)
{
++n;
++m;
}
~A()
{
--m;
}
};
A Func(A aa)
{
return aa;
}
int main()
{
A aa1;
A aa2;
cout << n << " " << m << endl;
A();
cout << n << " " << m << endl;
Func(aa1);
cout << n << " " << m << endl;
return 0;
}
这里n用来记录累计创建了多少个对象
m用来记录正在使用的还有多少个对象
这样子其实是不好的,要是我在main函数里面偷偷加上一个--n或者--m这里的数据就完全被打乱了
可能被外面随意修改
那我们放到类里面来
private:
int n = 0;
int m = 0;
这样子就会变成每个对象都有一个n和m,那不符合
加一个static它就属于每个对象了
private:
static int n;
static int m;
但是这里不能给缺省值,缺省值是要走初始化列表的,而初始化列表是某个对象的初始化列表,而它属于所有对象
而这个对象的大小应该是1,当做空类来看待,虽然它有两个成员变量但是这两个成员变量都不存在类里面,它存在静态区里面
同时上面这个声明,它还要有自己的定义
int A :: n = 0;
int A::m = 0;
声明和定义分离,这样我们就不能随意修改n和m了
如果我们把n和m改成共有的就有以下几种方式来帮助我们访问
A aa1;
A aa2;
cout << A::n << " " <<A::m << endl;
A();
cout <<aa1.n << " " <<aa2.m << endl;
A* ptr = nullptr;
cout << ptr->n << " " << ptr->m << endl;
这几种都是帮助它突破类域,它都不存在这些地方,它存在静态区,就是为了区分局部域和类域
这是公有的情况,私有的化上面的方式一种都不可以
那么我们只能像这样间接的去访问
int GetM()
{
return m;
}
int GetN()
{
return n;
}
void Print()
{
cout << n << " " << m;
}
A();
A();
那如果我想打印在这两个的n和m怎么办呢
这样的方式行不行呢
A().Print();
这属于是又创建了一个匿名对象
那我们引入静态成员函数
静态成员函数的特点:没有this指针
static int GetM()
{
return m;
}
static int GetN()
{
return n;
}
static void Print()
{
cout << n << " " << m;
}
A();
A();
A::Print();
这样就可以帮助我们突破类域了
那么静态成员函数有没有上面缺点呢,缺点是不能访问非静态,因为没有this指针
所以静态成员函数和静态成员变量一般都是搭配使用的
static int& GetM()
{
return m;
}
A();
A();
A::Print();
++A::GetM();
A::Print();
我们加了私有和保护只能说不能直接去访问,不是不能让你修改,是不能让你在类外面自己去访问,自己修改
六、内部类
概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限
注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员,但是外部类不是内部类的友元
特性:
内部类可以定义在外部类的public,protected,private都是可以的
注意内部类可以直接访问外外部类中的static成员,不需要外部类的对象/类名
sizeof(外部类)=外部类,和内部类没有任何关系
class A
{
public:
class B
{
private:
int _b;
};
private:
int _a;
};
int main()
{
cout << sizeof(A) << endl;
return 0;
}
B类受A类域和访问限定符的限制,其实他们两个是独立的类
所以A对象里面没有b
A aa;
A::B bb1;
如果是公有的就可以这样子访问,如果是私有的化就得通过间接的方式去访问
那内部类默认是外部类的友元又是说明意思呢,就是我自己开了块A田,A田里面有一个B鱼塘,鱼塘里面的鱼可以吃A田里面的蚯蚓什么的
class A
{
public:
class B
{
public:
void FuncB()
{
A aa;
aa._a = 1;
}
private:
int _b;
};
private:
int _a;
};
编译器的优化
A f2()
{
A aa;
return aa;
}
int main()
{
A ref1 = f2();
}
原本aa要拷贝生成一个临时对象再拷贝构造给ref,但是编译器觉得这样子太冗余了,所有直接拷贝构造给ref
A ref2;
ref2 = f2();
这样子就不行了
同类型才能优化,第一就是你们都是构造或者是拷贝构造才能优化,第二你们在两个步骤里面
规定只能在一个步骤里面优化
A f2()
{
return A(1);
}
A ref1 = f2();
这个也是直接构造
本质上也是构造一个临时对象,进行连续的构造和拷贝构造其实跟上面那种事等价的
A f2()
{
return 2;
}
这样子也行,也还是隐式类型转换
好了类和对象到这里就结束了,我感觉最难理解的的是隐式类型转换,因为套用了很多知识