前言
大家好,我是jiantaoyab,在这后面的学习中我们将进入C++类和对象的学习中,学习面向对象的思想,面向对象的三大特性是:封装、继承和多态
类的介绍
先来谈谈类,类的基本思想是数据抽象和封装,数据抽象是一种依赖于接口和实现分离的编程技术。封装实现了类和接口的实现和分离,类提供接口给用户使用,而用户是无法访问接口实现的细节。类就是一种封装,提供给用户访问的定义为公有,成员变量一般定义为私有。
类的定义
我先来实现一个基础的学生类,只有一个表示学号的id 和姓名
class Student
{
int id;
char name[5];
}
这不是和c中的struct很像吗?
是的c++是向前兼容了c的语法,区别就是c语言的结构体默认是公有的,而c++中的class默认是private。还有在struct中只是不同类型/相同类型数据的集合,是不能定义函数的,同时在struct中只能声明变量,不能进行初始化。
现在把上面的代码改成一个完整一点的class写法
class Student
{
private:
//成员变量
char _name[10];
int _age;
public:
//成员方法
//成员方法是放到公共的代码段
void Init(const char*name,int age)
{
strcpy_s(_name, name);
_age = age;
}
void Print()
{
cout << _name << endl;
cout << _age << endl;
}
};
成员函数的声明必须在类的内部,它的定义可以在类的内部,也可以在类的外部,定义在类的内部的函数是隐式的inline函数(忘记inline可以看看c++基础 )。
我们知道如果计算struct的大小,那如何计算类的大小呢?
上面打印出来的结果s1 =8; 我们从结果可以看出,在class中类对象大小的计算只计算成员变量的大小,而且采用结构体对齐规则成员函数是放到公共的代码段不计算大小。(忘记的可以看看c语言常考点 )
S2 =1;一个空类C++给了值1,这里的1不是存储的有效数据,只是为了占位,表示对象的存在
类的作用域
类的本身就是一个作用域,类的成员函数的定义嵌套在类的作用域之内。上面说到成员函数也是可以定义到类外的不过要加类的作用域解析符::指明是属于哪个类
class Student
{
public:
void PrintStudent();
private:
char _name[15];
char _gender[2];
int _age;
};
//类外定义
void Student::PrintStudent()
{
cout<<_name<<" "_gender<<" "<<_age<<endl;
}
this指针
大家有没有想过成员函数是怎么知道是哪个对象调用它的呢?
在C++编译器中给每个非静态的成员函数增加了一个名为this的隐式指针参数,让该指针指向当前对象(初始化),函数运行时调用该函数的对象。
在函数体中所有成员变量的操作,都是通过this指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
class Student
{
public:
void PrintStudent() // --> PrintStudent(Student* this)
{
cout<<_ name <<endl; // cout << this -> _name <<endl;
}
private:
char _name[15];
};
int main()
{
Student s1;
s1.PrintStudent(); // -- > s1.PrintStudent( & s1 );
}
这道题的结果是什么呢?
class A
{
public:
void PrintA()
{
cout<<_a<<endl;
}
void Show()
{
cout<<"Show()"<<endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
//崩溃 空指针不是语法错误,但是nullptr传给了this, cout<< *this->_a <<endl 就崩了
p->PrintA();
p->Show(); //打印 Show
}
this指针的特性
- this是const的,是不可以被修改的
- this指针一般情况下是存在栈中,某些编译器会存放到栈中
- this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this形参,所以对象中不存储this指针。
- 只能在“成员函数”的内部使用
类的默认成员函数
在类中,无论是不是空类,都会默认生成6个成员函数。我们先来看看构造函数和析构函数。
构造/析构函数
构造函数和析构函数是帮助我们在定义对象的时候自动完成初始化和资源清理的功能。
先来看看最简单的构造函数和析构函数,如果我们不显示的实现的话,编译器默认生成的就是长下面这个样子。
class Student
{
public:
Student() //默认构造函数
{}
~Student() //默认析构函数
{}
private:
int _id;
}
很明显,默认生成函数啥也不干,成员变量id也没有帮我们初始化为0。
但是如果在Strudent类中有一个类A,它就会自动跑去帮我调用A的默认构造函数初始化_A。
class A
{
A(int a = 2)
{
_a = a;
}
private:
int _a;
}
class Student
{
public:
Student() //构造函数
{}
~Student() //析构函数
{}
private:
int _id;
A _A; //自动调用了 A(int a = 2)
}
同样的析构函数也一样,所以在c++中默认生成的构造函数和析构函数对内置类型不处理,对自定义类型会调用它的默认构造函数/析构函数处理。
注意:
无参的构造函数和全缺省的构造函数,最后不要同时存在,会有歧义,一旦调用就会出错了。
class 无参
{
public:
Student() //无参的构造函数
{}
Student() //全缺省的构造函数
{
_id = 1;
}
private:
int _id;
}
初始化列表
使用初始化列表能方便我们初始化对象。
构造函数体中的语句只能将其称作为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
class Date
{
public:
//初始化列表是成员变量定义的地方
//const A&aa 传参的时候调用A(int a)去初始化
Date(int year=1,int month=1,int day=1,const A&aa)
:_year(year)
, _month(month)
, _day(day)
,_aa(aa)
{}
private:
int _year; //声明
int _month = 10; // c++11中,可以给缺省值,但是也是声明
int _day;
};
注意
-
每个成员变量在初始化列表只能初始化一次
-
类中包含下面成员的,必须初始化列表中初始化
引用成员变量,const成员变量,自定义类型成员(没默认构造函数的)变量
那么在类中初始化的顺序是什么呢?
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(); // 1 随机值
}
初始化顺序由定义类时的声明顺序决定,所以先初始化_a2,由于初始化_a2时_a1还未初始化,所以为随机值,故错误。所以建议大家声明和初始化的顺序保持一次,避免出现奇怪的错误。
再来思考一个问题,在类中构造和析构的顺序是什么呢?
先定义的先构造,后定义的先析构
设已经有A , B, C , D 4个类的定义,程序中A,B,C,D析构函数调用顺序为?( )
//全局和静态的数据放在数据段,最后才销毁的
C c; //全局
static F f //静态
int main(){
A a; B b;
static D d;
}
//构造的顺序是 c-f-a-b-d
//析构的顺序是 b-a-d-f-c
关键字explicit
用explicit修饰构造函数,将会禁止单参构造函数的隐式转换。
class Date
{
public:
Date(int year)
:_year(year)
{}
explicit Date(int year)
:_year(year)
{}
private:
int _year;
int _month:
int _day;
};
void TestDate()
{
Date d1(2024);
// 实际编译器背后会用2019构造一个无名对象,最后用无名对象给d1对象进行赋值
//这里编译器会将2次构造优化成一次
d1 = 2025; // 用一个整形变量给日期类型对象赋值,存在隐式类型转换
}
拷贝构造函数
在c中我们经常会有像x赋值给y这样的操作,同样的c++类中也有对象赋值给对象的操作,接下来我们来看看拷贝构造函数看看类中是如何赋值的。
class Student
{
public:
//必须把构造写出来,不然写了拷贝构造编译器就不会生成默认的拷贝构造了
Student(int id = 0)
{
_id = id;
}
Student(Student& stu)
{
_id = stu._id;
}
private:
int _id;
};
int main()
{
Student d1(123);
Student d2(d1);
}
可以看到,拷贝构造是构造函数的重载,但是在接受参数的时候用了引用,为什么这里要用引用呢?
假如我不用引用,传参的时候是传值传参,传值传参又是一个拷贝构造…就这样无限的调用下去了。不加引用编译都不通过。
编译器默认生成的拷贝构造,对于内置类型,会完成字节序的拷贝,也就是常说的浅拷贝,对于自定义类型会去调用它的拷贝构造函数。
class Student
{
public:
Student(int id = 0)
{
_id = id;
_name = new char[32];
}
Student(Student& stu)
{
_id = stu._id;
_name = stu._name;
}
~Student()
{
delete []_name;
}
private:
int _id;
char* _name;
};
int main()
{
Student d1(123);
Student d2(d1);
}
可以看到,程序崩了,是因为对同一块资源释放了2次,所有对于有开空间的对象,我们要完成深拷贝。
下面代码共调用了多少次拷贝构造函数?
拷贝构造是发生在对象还没被定义出来的时候给发送的,而赋值拷贝是2个已经存在的对象发生的。
拷贝构造发生时机
1.将一个对象作为实参传递给一个非引用类型的形参
2.从一个返回类型为非引用类型的函数返回一个对象
3.用花括号列表初始化一个数组中的元素或一个聚合类中的成员
Widget f(Widget u)
{
Widget v(u);
Widget w=v;
return w;
}
int main(){
Widget x;
Widget y=f(f(x));
}
上面的代码一共调用了7次拷贝构造,编译器会对连续的拷贝构造函数进行优化。
运算符重载和赋值拷贝
假如我想比较出2个日期的大小,比如2023年1月1日和2024年3月15日哪个日期大,在c++中并没有运算符能比较我们自定义类型的大小,c++也不支持自定义类型使用运算符,所以需要运算符重载。
我们来实现一个日期类,在日期类中学习怎么使用运算符重载。先新建Date.h 、Date.cpp、test.cpp三个文件。
Date.h
#include<iostream>
using namespace std;
class Date
{
friend ostream& operator<<(ostream& out, const Date& d);
public:
Date(int year , int month , int day);
//bool operator>(Date* const this, const Date& d2)
bool operator>(const Date& d2)const;
//赋值拷贝实现
Date& operator=(const Date& d);
//void operator<<(ostream& out); 写成成员函数的话调用的时候 是 d1<<out 很反人类
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, const Date& d);
Date.cpp
#include"Date.h"
Date::Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//bool operator>(Date* const this, const Date& d2)
bool Date::operator>(const Date& d2)const
{
if (_year > d2._year) return true;
else if (_year == d2._year && _month > d2._month)
return true;
else if (_year == d2._year && _month && d2._month && _day && d2._day)
return true;
else
return false;
}
//赋值拷贝实现
Date& Date::operator=(const Date& d)
{
//相同就不拷贝
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
//void Date::operator<<(ostream& out)
//{
// out << _year << ":" << _month << ":" << _day;
//}
ostream& operator<<(ostream& out, const Date& d)
{
out <<d._year << ":" << d._month << ":" << d._day;
return out;
}
test.cpp
#include"Date.h"
int main()
{
Date d1(2001, 1, 1);
Date d2(2003, 1, 2);
d2 > d1; //operator(d1 > d2) d1.operator(d2);
d2 = d1; //赋值拷贝
cout << d1;
getchar();
}
通过上面的代码我们已经学会了运算符重载和赋值拷贝,再来看看要注意地方。
注意
- 这几个符号是不能重载的 .* :: sizeof ?:(三目运算符) .
- 默认的赋值重载和默认的拷贝构造是一样的都是浅拷贝。
- 双操作数的运算符重载时候,默认第一个参数是左操作数,第二个操作数是右操作数字。
取地址和const取地址操作符重载
一般这2个成员函数用的很少,大家知道就好。
class A
{
public:
A* operator&()
{
return this;
}
const A* operator&() const
{
return this;
}
}
类的6的默认成员函数介绍完了,接下来看看static 和 友元,我写过一篇static详解大家可以点进去看看里面有十分详细的介绍。
友元
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
大家不知道还记得上面的想要实现打印出日期类的private的成员变量的时候,我在类外重载operator<<函数的时候使用了友元,能够看出友元能够帮助我们访问类中的private成员,破坏了类的封装性。
友元函数
class Date
{
friend ostream& operator<<(ostream& _cout, const Date& d);
friend istream& operator>>(istream& _cin, 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& _cout, const Date& d)
{
_cout<<d._year<<"-"<<d._month<<"-"<<d._day;
return _cout;
}
注意:
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元不具备this指针
友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
注意:
- 友元关系是单向的,不具有交换性。
- 友元关系不能传递,如果B是A的友元,C是B的友元,则不能说明C时A的友元。
class A
{
friend class B; //在a里说b是a的朋友,就可以在b中调用a的私有成员变量
private:
int _a;
};
class B
{
private:
int _b;
};
内部类
如果一个类定义在另一个类的内部,这个内部类就叫做内部类。注意此时这个内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去调用内部类。外部类对内部类没有任何优越的访问权限
class A
{
public:
class B
{
//B像友元一样访问A的私有成员,但是A不能访问B
void PrintA(const A& a)
{}
private:
int _a;
};
注意:
内部类就是外部类的友元类。注意友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元 。
结束
好了类和对象基本介绍完了,欢迎大家补充。