类和对象(一)(C++)
类和对象:
类的引入:
C语言结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。比如: 之前在数据结构初阶中,用C语言方式实现的栈,结构体中只能定义变量;现在以C++方式实现, 会发现struct中也可以定义函数
typedef int DataType;
struct Stack
{//成员函数void Init(size_t capacity){_array = (DataType*)malloc(sizeof(DataType) * capacity);if (nullptr == _array){perror("malloc申请空间失败");return;}_capacity = capacity;_size = 0;}void Push(const DataType& data){// 扩容_array[_size] = data;++_size;}DataType Top(){return _array[_size - 1];}void Destroy(){if (_array){free(_array);_array = nullptr;_capacity = 0;_size = 0;}}//成员变量DataType* _array;size_t _capacity;size_t _size;
};
但c++更喜欢用class来代替:
类的两种定义方式:
1. 声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内 联函数处理。
2. 类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名::,
这些在之后都会经常用到
成员变量命名规则:
class Date{public:void Init(int year){// 这里的year到底是成员变量,还是函数形参?
year = year;}private:int year;};
为了区分开来,会在成员变量前加_来区分,也可以用其他方式
类的访问限定符:
1.pubic,公有 2.protected,保护 3.private,私有
1. public修饰的成员在类外可以直接被访问
2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
4. 如果后面没有访问限定符,作用域就到}即类结束。
5. class的默认访问权限为private,struct为public(因为struct要兼容C)
我们一般对成员变量设为私有,成员函数设为公有
如何计算类的大小:
一个类的大小,实际就是该类中”成员变量”之和,当然要注意内存对齐,成员函数会放在公共的代码段,因为多个对象调用同一份函数,如果按照此种方式存储,当一 个类创建多个对象时,每个对象中都会保存一份代码,相同代码保存多次,浪费空间
但空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象,即证明这个类存在
class A
{};
int main()
{cout << sizeof(A) << endl;return 0;
}
this指针:
特点:
1. this指针的类型:类类型* const,即成员函数中,不能给this指针赋值
2. 只能在“成员函数”的内部使用
3. this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给 this形参。所以对象中不存储this指针,this存储在栈上
4. this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传 递,不需要用户传
我们在类中也可以直接使用this指针来更好的理解
void Print()
{
// this = nullptr;
cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};int main()
{
Date d1;
Date d2;
d1.Init(2024, 4, 2);
d2.Init(2024, 4, 3);
}
this指针可以为空吗?
public:void Print(){cout << "Print()" << endl;}private:int _a;};int main(){A* p = nullptr;p->Print();return 0;}
class A{
public:void PrintA() {cout<<_a<<endl;}private:int _a;};int main(){A* p = nullptr;p->PrintA();return 0;}
以上两个代码,第一个不会报错,第二个报错,原因是第一个没有对this指针解引用,而第二个对this空指针解引用了,会进行报错
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员 函数。 默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数
类的6个默认成员函数:
1.构造函数:
构造函数的主要任务并不是开空间创建对象,而是初始化对象
特征:
1. 函数名与类名相同。
2. 无返回值。
3. 对象实例化时编译器自动调用对应的构造函数。
4. 构造函数可以重载。
class Date
{
public:Date()//无参,打印值默认{}Date(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;// 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明d1.Print();Date d2(1, 2, 3);d2.Print();return 0;
}
如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦 用户显式定义编译器将不再生成。
C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类 型,如:int/char...,自定义类型就是我们使用class/struct/union等自己定义的类型,对于内置类型,没有规定要不要做处理(部分编译器会处理),对自定义类型才会调用无参构造
但:
C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在 类中声明时可以给默认值
class Time{private:int _hour;int _minute;int _second;};
class Date{private:// 基本类型(内置类型)int _year =1;int _month = 1;int _day = 1;// 自定义类型
Time _t;};int main(){Date d;return 0;}
内置类型可处理可不处理,自定义类型会调用默认构造,如果没有,就会报错(可以进行自定义类型的多重嵌套)
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。 注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。否则会存在使用冲突
2.析构函数
特点:
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值类型。
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构 函数不能重载
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
当没有显式析构函数时,编译器自动生成的析构函数,对内置类型不做处理,自定义类型调用析构函数
class Time{public:~Time(){cout << "~Time()" << endl;}private:int _hour;int _minute;int _second;};class Date{private:// 基本类型(内置类型)int _year = 1970;int _month = 1;int _day = 1;// 自定义类型
Time _t;};int main(){Date d;return 0;}
如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如 Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类
stack::~stack(){if (_a) {cout << "Destroy" << endl;_capacity = 0;_top = 0;free(_a);_a = NULL;}}
3.拷贝构造
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存 在的类类型对象创建新对象时由编译器自动调用
特点:
1. 拷贝构造函数是构造函数的一个重载形式。
2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错, 因为会引发无穷递归调用。
class Date
{
public:void Print(){cout << _year << " " << _month << " " << _day << endl;}Date(int year=1,int month=1,int day=1) {cout << "构造" << endl;_year = year;_month = month;_day = day;}Date(const Date& d)//防止被修改{cout << "拷贝" << endl;_year = d._year;_month = d._month;_day = d._day;}
private:int _year=1 ;int _month=1;int _day=1;
};
int main()
{Date d;d.Print();Date d1=d;//Date d1(d);两者等价d1.Print();return 0;
}
拷贝构造参数用const 修饰,防止被修改
为了理解无穷递归,在这里举个例子
void func(Date& d)
{d.Print();
}
int main()
{Date d2;func(d2);return 0;
}
这里用引用传递实参和形参都是d2,而值传递的话会先拷贝d2给d,进入到拷贝函数中d2和d地址也不同
若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请 时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
如stack中的深拷贝:
stack::stack(const stack& st){_a = (int*)malloc(sizeof(int) * st._capacity);if (_a == nullptr){return;}_capacity = st._capacity;_top = st._top;memcpy(_a, st._a, _capacity * sizeof(int));//深拷贝}//不这样写只是浅拷贝
4.运算符重载:
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其 返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似
函数名字:关键字operator后面接需要重载的运算符符号。
注意:
不能通过连接其他符号来创建新的操作符:比如operator@
作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐 藏的this
.* :: sizeof ?: . 注意以上5个运算符不能重载。
.*很少见,下面给一个场景
class OB
{
public:void func(){cout << "void func()" << endl;}
};typedef void(OB::* PtrFunc)();//成员函数指针类型int main()
{PtrFunc fp = &OB::func;//定义成员函数指针p指向函数funcOB temp;//定义ob类对象temp(temp.*fp)();return 0;
}
运算符重载成全局的就需要成员变量是公有的,封装性不能保证。
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;
}
这里先写一个判断日期相等的函数,可以看到,如果写成全局的函数,私有的变量就需要变为公有,所以我们就重载成成员函数
这里可以用我们用重载成成员函数解决
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}bool operator==(const Date& d){return _year == d._year&& _month == d._month&& _day == d._day;}
//private:int _year;int _month;int _day;
};
赋值运算符重载:
格式:
参数类型:const T&,传递引用可以提高传参效率
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值 ,检测是否自己给自己赋值
返回*this :要复合连续赋值的含义
对返回值类型进行深究:
传值返回:生成一个拷贝作为返回值
传引用返回:不会拷贝
Date func()
{Date d;return d;//出作用域析构d
}//传值返回临时对象,具有常性,需要拷贝
int main()
{Date& ref = func();//报错const Date& ref = func();//正确return 0;
}
这里使用传值返回, 在func中建立d,返回d的值时会拷贝d的值给一个临时变量,调用拷贝函数,临时变量具有常性,需要const 接收,并且d的值,出函数作用域会调用析构
Date& func()
{Date d;return d;
}
int main()
{Date& ref = func();//ref类似于野指针了,指向的空间可能会改变return 0;
}
这里传引用是有问题的,d出作用域就会销毁,再返回其地址类似于野指针了,指向空间可能会改变
但如果改成这样的话,就不会报错了,因为d相当于在全局域了
Date& func()
{
static Date d;return d;
}//可以引用返回
所以总结一下,
返回对象是局部变量或临时对象,用引用返回存在风险,虽然减少了拷贝
返回对象是全局变量,用引用就行出了作用域,返回对象还在没有析构,那就可以用引用返回,减少拷贝
a、返回对象生命周期到了,会析构,传值返回
b、返回对象生命周期没到,不会析构,传引用返回
连续复制,如果知识两个值进行运算的话,我们是可以将返回类型设为void的,但d1=d2=d3怎么办呢,这个式子从右向左依次赋值,所以返回类型要修改为Date&
Date& operator=(const Date& d)//void不支持三个数连续等于//传值返回会拷贝构造{cout << "赋值" << endl;if (this != &d)//防止自己给自己赋值{_year = d._year;_month = d._month;_day = d._day;}return *this;}
5..取地址及const取地址操作符重载
class A {
public:A* operator&(){return this;}const A* operator&()const {return this;}
};
int main()
{A a1;const A a2;cout << &a1<< endl;cout << &a2 << endl;return 0;
}
这两个函数重载依靠this指针类型的不同,但我们不写编译器会自动生成
日期类的实现:
class Date
{
public:friend ostream& operator<<(ostream& out, const Date& d);///ostream抢不过第一个参数形参//istream& operator>>(const Date& d);参数问题friend istream& operator>>(istream& in,Date& d);Date(int year = 1900, int month = 1, int day = 1);int CheckDate(){if (_month > 12 || _month < 1||_day>Get_day(_year,_month)){cout << "输入错误" << endl;return 1;}else{return 0;}}void Print();int Get_day(int year, int month){assert(month > 0 && month < 13);static int data[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };//多次调用if (month==2&&(year%100==0&&year%4!=0||year%400==0)){return 29;}return data[_month];}Date& operator+=(int day);Date operator+(int day)const;Date& operator-=(int day);Date operator-(int day)const;Date operator++(int);Date operator--(int);Date& operator--();Date& operator++(); bool operator>(const Date& d)const;bool operator>=(const Date& d) const;bool operator<=(const Date& d) const;bool operator<(const Date& d) const;bool operator==(const Date& d) const;bool operator!=(const Date& d) const;int operator-(const Date& d) const;private:int _year;int _month;int _day;
};
Date::Date(int year, int month, int day)
{_year = year;_month = month;_day = day;
}
void Date::Print()
{cout << _year <<" "<< _month <<" "<< _day << endl;
}
Date& Date::operator+=(int day)
{if (day < 0){*this-=-day;return *this;}_day += day;while (_day > Get_day(_year, _month)){_day -= Get_day(_year, _month);_month++;if (_month == 13){_month = 1;_year++;}}return *this;
}
Date Date::operator+(int day) const
{Date tmp = *this;tmp += day;return tmp;
}
Date& Date::operator-=(int day)
{if (day < 0){*this += -day;return *this;}_day -= day;while (_day <= 0){--_month;if (_month == 0){_month = 12;_year--;}_day += Get_day(_year, _month);}return *this;
}
Date Date::operator-(int day) const
{Date tmp = *this;tmp -= day;return tmp;
}
Date Date::operator++(int)
{Date tmp = *this;(*this) += 1;return tmp;
}
Date Date::operator--(int)
{Date tmp = *this;(*this)-=1;return tmp;
}
Date& Date::operator--()
{(*this) -= 1;return *this;
}
Date& Date::operator++()
{(*this) += 1;return *this;
}
bool Date::operator>(const Date & d) const
{if (_year> d._year){return true;}else if (_year == d._year){if (_month > d._month){return true;}else if (_month == d._month){if (_day > d._day){return true;}}}return false;
}
bool Date::operator>=(const Date& d) const
{return *this == d || *this > d;
}
bool Date::operator<=(const Date& d)const
{return !(*this > d);
}
bool Date::operator<(const Date& d)const
{return !(*this >= d);
}
bool Date::operator==(const Date & d)const{return _year == d._year && _month == d._month && _day == d._day;}
bool Date::operator!=(const Date & d)const{return !(*this == d);}
int Date:: operator-(const Date& d)const//算有多少天
{Date big = *this;Date small = d;//拷贝构造int flag = 1;if (big < small){big = d;small = *this;flag = -1;}int n = 0;while (small < big){small++;n++;}return n*flag;
}
ostream& operator<<(ostream& out, const Date& d)
{out << d._year <<" "<< d._month <<" "<< d._day << endl;return out;
}
istream& operator>>(istream& in, Date& d)//传引用,不支持拷贝构造,被禁用了
{cout << "请输入年月日" << endl;in >> d._year >> d._month >> d._day;if (d.CheckDate()){cout << "日期非法" << endl;}return in;
}
日期类的实现有一些需要注意的点,我在这里一一说明:
1.对于多次调用的Get_day函数,我们直接在类中展开写,其会自动转化为内联函数,包括data数组,我们也要多次使用,所以在前面加上static修饰,变为全局变量
2.对于*this不能改变的函数,我们在函数声明后加上const修饰(比较特殊),可以发现,传引用返回并且对*this进行操作的一般不加const修饰
3.前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载 , C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器 自动传递
4.cout和cin无法对自定义成员进行处理,进行运算符重载要将其转化为友元类才符合输出格式(重载为全局破坏封装性,重载为成员函数(不是友元)输出格式不符合,第一个参数为this形参,不能是cout或cin)