C++类和对象
前言:
续上篇第四个默认成员函数赋值重载开始讲起,讲完这个知识,C++中六个默认成员函数中最重要的四个我们都讲了。
一.赋值重载
1.1运算符重载
在讲赋值重载之前要先了解C++运算符重载知识,编译器对内置类型比如int、double类型直接使用运算符,比如整型的大小比较,赋值等等。
这是因为对于内置类型,编译器知道它的比较规则以及处理方法。而对于自定义类型如何进行比较和处理,编译器是不清楚的。在C语言中使用比较函数进行比较:
C++也是实现比较函数完成比较的,但是Date类的成员变量是私有,无法在全局中使用,有没有什么解决方法呢?
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//定义在类里面,对象就可以访问私有成员了
bool Equal(const Date& d)//成员函数有隐藏的this指针
{
return (_year == d._year &&
_month == d._month &&
_day == d._day);
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 1, 22);
Date d2(2023, 1, 21);
d1.Equal(d2);
return 0;
}
函数名Equal的可读性还是可以接受的,但是有不省心的人用奇奇怪怪的函数名,比如Compare,更有甚者xiangdeng。
祖师爷想:规定一个函数名命名方法,于是operator运算符做函数名出现了。
这样一来,当d1.operator==(d2)出现的时候,大家一眼就知道要进行相等判断。
但是做到这一步后,祖师爷觉得还没达到预想的效果,就干脆让d1和d2像内置类型一样使用d1==d2进行相等判断。
在这个过程中实现了相等运算符的重载,本来是用来判断内置类型的,**当在类中实现了运算符,类对象就可以像内置类型一样使用==**,以上就是运算符重载的知识。
关于运算符重载:operator后面加的运算符应该是有在使用的(+、-、>、<、!…等等等),不能自创。比如:@本来就不是运算符,不能operator@进行重载。
1.2赋值重载
讲完运算符重载,我们来讲赋值重载。当在类中定义有相应的运算符重载,类对象才能像内置类型一样使用,如果没有定义就使用则会报错!
那么没有定义赋值运算符,能不能使用呢?
答案是能的,因为它是默认成员函数,不显示定义编译器会生成一个。学完运算符重载,我们就可以自己显示定义一个赋值重载:
1.首先函数名是operator=;2.函数参数是同类对象的常引用,比如 d1 = d2的时候会调用d1.operator(d2)。如果形参不用引用就会多一次拷贝构造(笋),使用const是为了防止赋值逻辑写反了。
对于运算符,基本都有返回值,自定义类型的运算符重载参照内置类型的行为,比如:
int main()
{
int i = 0, j = 0;
i = j = 10;//=运算符的结合性是从右向左,j=10的返回值是j(int)
//i = (j=10)的返回值,最后i也被赋值成10,然后返回i,只是i没有接收
return 0;
}
所以,d1 = d2的时候会返回d1。赋值时d1出了赋值重载函数,生命周期并不会结束,因为它不是赋值重载里的局部变量,使用引用返回可以减少拷贝构造。
这里this指针虽不能显示传参,但是可以在成员函数中显示使用。要返回d1对象,恰好*this就是d1,这个场景就是说明为什么在成员函数内可以显示使用this的原因。
最后:我们不显示定义,编译器生成的赋值重载,对内置类型实现值拷贝,对自定义类型去调用它的赋值重载。
经常把构造函数和析构函数看成一对、拷贝构造和赋值重载看成一对。不论是拷贝构造还是赋值重载,它们只要在程序中出现调用,就相当于进行了一次拷贝,这也是衡量一些代码效率好或差的指标之一,拷贝少程序的更好。
二.const成员函数
当涉及到const对象调用成员函数时,this指针所指的对象是const类型,该成员函数需要加上const才能调用。
这里可以看到d2<d1是可以正常调用的,因为d2的地址传递给this指针,this的类型是Date* const this,因此Date*传递给Date*是不会有问题的。
但是d1是const类型,传d1地址的类型是const Date*,将const Date*交给Date*就是权限的放大,因此编译不通过。
但是this指针在设计的时候就不允许我们自己显示传递,如何将const加在Date* const this的最前面呢?祖师爷这样规定:
声明和定义分离的时候,都要在函数最后面加const。记住const成员函数针对的是this指针,给this指针加的const,因此如果不是成员函数,没有这种写法。
以前说过const对象只能传给const对象,这是权限的平移;非const对象传给const对象,这是权限缩小;
看到这里我们给所有的比较运算符重载都加上了const,因为这些运算符不会改变对象,加上是没问题的。而对于会改变成员变量的函数不能加const。
2.1最后的默认成员函数
讲完const成员变量,我们再来说说C++中剩下的最后两个默认成员函数,它们是:
取地址运算符重载和const取地址运算符重载,把这两个做成默认成员函数可能是因为祖师爷觉得这两个运算符重载没有专门写的必要,但是类又需要使用&取地址,所以让它们成为默认成员函数,不用写也能用。
讲到这里默认成员函数六个就齐了,前面四个才是重点,我们接着往下。
三.再谈构造
初始化列表是构造函数里用来定义成员变量的地方,读者可能会问:什么?构造函数还没讲完!?是的,但这点讲完就结束了。
定义成员变量的地方给成员变量给值,就是初始化。前面在函数体内赋值,本质是给成员变量一个初始值,但并不是初始化。可以按下面理解:
初始化列表初始化就相当于一个机器人的出厂设置,然后到了函数体(商家)那里再进行一次设置(函数体初始化),比如让机器人成为扫地的、放歌的等等专职机器人、最后到买家手里使用。
注意初始化列表和函数体可以混着用,那么这个初始化列表到底是用来干什么的?我们接着往下看:
首先说说祖师爷对初始化列表的语法设计:1.使用冒号开始,成员变量后面加个括号,里面放值或表达式;2.随后的其它成员变量使用逗号隔开;
并规定初始化列表是成员变量定义的地方,处理引用、const对象要在定义的时候初始化问题:
在函数体内的赋值不是初始化,而是相当于赋值!所以上面的引用和const对象没有进行初始化,应该按下面这样:
尽量使用初始化列表对成员变量进行初始化,因为不管我们有没有显示给成员变量初始化,编译器都会走初始化列表,我们在函数体内赋值就相当于下面这样:
int main()
{
int i;
i = 0;
return 0;
}
前面讲过构造函数对内置类型不做处理,对自定义类型调用它的默认构造,这些都是在初始化列表里做的,接着往下看:
class Stack
{
public:
Stack()
{
cout << "Stack()" << endl;
}
private:
int* _a;
int _top;
int _capacity;
};
class MyQueue
{
public:
private:
Stack st1;//调用Stack类的默认构造
Stack st2;
int size = 1;
};
int main()
{
MyQueue mq1;
return 0;
}
假设Stack没有默认构造就会报编译错误,因为初始化列表就是对自定义类型调用它的默认构造,如果没有默认构造,它不知道该怎样创建这个对象。
那么换言之,让MyQueue类默认构造的初始化列表知道,Stack类的构造函数要传什么参数,就可以调成功了。
没错,初始化列表可以解决自定义类型没有默认构造的问题!
再理顺一遍:上图中的MyQueue类我们写了一个默认构造(无参),然后创建mq1对象,该对象调用它自己的默认构造。
在初始化列表处定义st1要去调用Stack类的默认构造,发现Stack类没有默认构造,但没关系,我们显示写的初始化列表中有传参数。
但是如果我们没有写MyQueue类的构造函数,编译器生成的构造函数看到Stack类没有默认构造,就报错,因为编译器不知道n要传多大。
初始化列表解决了程序员如果有需要,对自定义类型可以进行控制,不会让默认的随便走。
最后讲一下初始化列表的细节:内置类型成员变量声明的地方给缺省值,缺省值在初始化列表的时候被使用。
d1传了三个参数,但是没用到。走初始化列表,没有显示初始化,使用声明时给的缺省值。但是初始化列表我们显示写了的话,那和缺省值就无关了,看下面代码:
博主给这五副截图,就是要说明成员变量的初始化顺序不是由初始化列表中写的顺序说了算,而是由成员变量的声明顺序说了算,看上面是按_year、_day、_month顺序进行初始化的。
四.静态成员
静态成员包括静态成员变量和静态成员函数,它们是怎么使用的呢,我们看以下代码:
class A
{
public:
A() { count++; }
A(const A& aa) { count++; }
static int GetCount()//静态成员函数
{
return count;
}
private:
static int count;//静态成员变量的声明
};
int A::count = 0;//静态成员变量的初始化
A Func()
{
A a;
return a;
}
int main()
{
Func();
cout << A::GetCount() << endl;
return 0;
}
静态成员变量它是受限制的全局变量,它属于整个类,每个类对象都可使用它;
不能给缺省值,因为缺省值是给对象成员变量初始化用的,而静态成员变量不属于单个对象,属于整个类,定义要在类外定义,并且不需要加static,声明的时候给过了。
而静态成员函数没有this指针,因此在静态函数中无法使用成员变量和成员函数。
五.友元
友元是一个增加代码耦合的操作,有两种分别是友元函数和友元类。
class Date
{
//友元函数声明
friend ostream& operator<<(ostream& out, const Date& d);
//友元类声明
friend class A;
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
class A
{
public:
static void Print(const Date& d)//静态成员函数,没有this指针
{
cout << d._year << "年" << d._month << "月" << d._day << "日" << endl;
}
private:
int _a;
};
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
int main()
{
Date d1(2023, 1, 24);
cout << d1;//流插入运算符重载
A::Print(d1);//可以指定类域进行访问
}
A类是Date类的友元,也就是A是Date类的朋友,因此A类可以访问Date的私有,但是Date类不能访问A类的私有,友元具有单向性。
友元函数是说在该函数内可以访问类的私有。
六.类部类
在类中可以再定义普通类,它和类中的静态成员变量,静态成员函数一样,就只是对了外部类类域和访问限定符的限制,并且内部类天生是外部类的友元!
class A
{
public:
//天生是A的朋友,可以访问它的私有
class B
{
public:
void Print(const A& aa)
{
cout << aa._a << endl;
}
private:
int _b;
};
private:
int _a = 10;
};
int main()
{
A a;
A::B b;//
b.Print(a);
}
讲到这里,类和对象的知识就讲完啦!希望读者有所收获。