类和对象
C语言是面向过程语言,c++是面向对象语言。
面向对象:拆解出对象,依靠对象和对象之间的交互 ; 对象——>实体 ; 类:描述对象
类的定义方式:
1.声明定义全部放在类中
2.类中只做声明,定义放在类外,用 ClassName::function(){}定义(推荐这一种,类起到封装的作用,不必展示函数实现细节,只要提供接口函数)
C++中struct和class是等价的,不过struct默认访问权限为public(兼容C语言),类中默认权限为private
面向对象程序设计三大特性:封装 、 继承 、 多态
封装:将数据和操作数据的方法记性有机结合,隐藏对象属性和实现细节(private),仅对外公开接口来和对象进行交互(public)
访问权限有三种:public、private、protected
类也是一个作用域,c++中有四种作用域:全局作用域,函数体内部的局部作用域,命名空间,类域
类的实例化:用类类型定义对象的过程
对象中只存储成员变量,成员函数是共享的,那么就存在一个问题,没有给函数显示传递对象的地址或者其他信息,怎么知道要操作的对象
对象的大小:
空类的对象,默认1个字节来区分该空类创建的不同对象(主流编译器)
注意空类并不是什么都没有,编译器会给改了自动生成一些默认的成员方法(构造、析构、拷贝构造、赋值重载、取地址重载)
#include<iostream>
#include<string>
using namespace std;
class Washmachine
{
private:
double _length;//8字节
double _height;
double _width;
public:
Washmachine(){}
double getLength()
{
return _length;
}
void setLength(double length)
{
_length = length;
}
};
class A{};
int main()
{
cout << sizeof(Washmachine) << endl;
Washmachine w1;
w1.setLength(1);//没有传递w1对象的地址,怎么操作的w1对象
cout << sizeof(w1) << endl;//24字节,对象中并不存储函数,只存储成员变量,函数由
/*函数编译完成后,存储函数的地址是确定的,编译器肯定是可以找到的
通过符号表找到,符号表中存储的就是名字和地址的映射关系*/
A a1,a2,a3;
cout << sizeof(a1) << endl;//空类大小为1,要标记这个对象的位置,空间为0,会造成多个定义对象都在同一位置,无法区分
return 0;
}
this指针
下面来讨论函数是如何获取要操作的对象信息(隐式传递的this指针)
#include<iostream>
using namespace std;
//在C++中, 类和结构是只有一个区别的:
//类的成员默认是private,而结构是public。
class Date
{
public:
Date();
Date(int year, int month, int day )
{
//Date * & p = this;//类型不对
//Date* const& p = this;//引用取别名
auto p = this;
cout << p << endl;
this->year = year;
this->month = month;
this->day = day;
cout << this << endl;
cout << typeid(this).name() << endl;//this 类型:class Date *
//this = nullptr;//不可修改说明该指针应为常量,即指针常量,不可修改指向
}
~Date();//析构函数,释放资源,不是释放对象空间
void print();
/*成员函数默认第一个参数为 :T * const this(类指针常量)
this的效率相对于手动传递参数给静态函数效率会更高一些,编译器会优化,比如用到寄存器
*/
private:
int year;
int month;//成员变量名,和构造函数中的参数名可以用_区分,当然也可以不区分,不过在赋值时要用this标记是成员变量,而不是形参
int day;
};
Date::Date()
{
}
Date::~Date()
{
}
void Date::print()
{
cout << this->year << "年" << this->month << "月" << this->day << "日" << endl;
}
int main()
{
Date d1;//调用空构造方法,不要带括号
Date d2(2022, 11, 13);//调用构造方法,但构造方法只是初始化对象,不是开辟空间创建对象
d1.print();
d2.print();//c++编译器在执行时会传递这个调用对象的地址
//this指向调用该方法的对象
//哪个对象调用成员函数,就会在其调用函数时,隐式传递第一个参数为this指针,this关键字指向该对象的地址,this并不属于某个对象
//无法通过对象来调用this,但是在成员函数中可以用this
return 0;
}
打印不出来this实际的类型,但是通过引用发现this的类型:Class * const 类指针常量
六大默认成员函数:
1.构造函数:特殊的成员函数
函数名必须与类名相同,并且不能有返回值类型
在创建对象时有编译器调用, 在整个对象的生命周期内只调用一次
可以重载,因为有参数,下面的析构函数无法重载,因为没有参数
目的:给对象中的成员变量设置合适的初始值
在类中,如果用户没有显式定义任何构造函数,则编译器一定会生成一份无参的构造函数,如果显示定义了带参构造函数,若要Date d1;此时还要显示定义加上无参构造
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个,实际上全缺省默认构造函数包含了无参构造的功能
#include<iostream>
using namespace std;
class Time
{
public:
Time();
private:
int hour;
int mintue;
int second;
};
Time::Time()
{
this->hour = 16;
this->mintue = 30;
this->second = 29;
cout << "" << endl;
}
class Date
{
public:
void print();
private:
int _year;
int _month;
int _day;
Time t1;//有了这个成员后,此时下面的构造方法就会被调用
};
void Date::print()
{
cout << this->_year << "/" << this->_month << "/" << this->_day << endl;
}
int main()
{
//再次强调:构造方法只是对成员函数进行初始化,不为对象开辟空间
Date d;//直接跳过这一步了,不生成构造方法,因为没有初始化,虽然语法规定要生成,但编译器优化以后,发现不生成也能达到同样的效果(不同编译器实现不同)
/*
当成员变量有了Time ti对象时,对于Time有显示定义的构造方法,此时就要调用构造方法
要对t1进行初始化,所以要调Time的构造方法,那么由谁调用呢,就要用Date构造方法调用(t1也是Date的成员要初始化,自然要调用)
内置类型和自定义类型处理上的区别
当然如果Time没有显示定义构造方法,那么就不用初始化t1,则d的成员变量都不用初始化,所以也就不必调用构造方法
*/
d.print();
return 0;
}
2.析构函数:~类名();给构造函数名前加上一个 ~
如果对象中没有涉及到任何资源管理时,该类的析构函数可以不用给出
析构函数只是用来释放资源,不是销毁对象空间(对象在销毁时会自动调用析构函数,完成对象中资源的清理工作)
无法重载,只能有一个
3.拷贝构造函数:
用已创建的对象创建新对象时,将就对象做参数传递,调用拷贝构造函数
例:Date d2( d1);//d1已经创建了,可以看出拷贝构造函数是构造函数的重载函数,参数必须是类类型对象的引用(最好将参数设置为const,用const修饰防止对原对象进行修改)参数只有一个且必须是类类型对象的引用
若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成
拷贝–浅拷贝(值拷贝)
不可设置参数类型为 类类型
错误做法:
Date (const Date d){};这样会引发无穷递归,当然这样写出来时编译器就会报错,
这样的传值操作,要先构造临时对象,要调用拷贝构造,而这个构造的参数就又要创建临时对象,才能进行值拷贝…一直这样构造下去,无穷递归
当涉及到资源管理时,拷贝构造函数一定要显示定义,默认的是浅拷贝,会造成堆区的资源实际只会被申请一次,新对象的指针指向的是源对象申请的堆空间,在析构时就会造成内存多次释放,造成程序崩溃
对于拷贝构造函数,有时也会出现代码中调用它,但是在汇编中查看实际并没有生成,只是逐字节的值拷贝,完成了拷贝构造的工作
在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调
用其拷贝构造函数完成拷贝的,此时必然要调用Date类的拷贝构造,要用Date类的拷贝构造函数取调用Time类的拷贝构造函数
当要用对象传参时,最好用引用传参,这样就避免了隐式的拷贝构造,不必生成多余的对象,从而提高效率;函数返回值为匿名对象时,也能够提高效率,减少拷贝构造次数
4.赋值运算符重载
先看下面代码
d2 = d1;这样写难道不会报错吗?在之前的理解中只有像int,double等内置类型才能进行这样的赋值运算,像类结构体这一种自定义类型是不可以的。
d2 =d1;==》d2.operator(d1);
其实这里是一种赋值运算符重载
赋值运算符重载:用已经存在的对象 给 另一个已经存在的对象赋值(前提是二者都已存在)像这样Date d2=d1;这不是运算符重载,这是一种拷贝构造形式
语法:如果程序员没有显示定义赋值运算符重载,则编译器就会自动生成一份
实际情况:编译器不一定会生成,但是编译器一定会完成赋值的工作(上面的代码就是),这种默认但单纯值拷贝(将对象1中的内容原封不动的搬移到对象2),当遇到内存管理时,必然也会向默认的拷贝构造一样在析构时会出问题
赋值运算符只能重载成类的成员函数不能重载成全局函数
(因为在类外定义就要传递两个参数,但是当类中无定义时又会默认生成,这时其实有两个完全相同原型的函数,重载冲突)
运算符重载:
具有特殊函数名的函数,有返回值,函数名,参数列表
定义:返回值 operator 运算符 (){}
例:==这个函数是在类内定义的,如果在类外定义,就无妨访问类内的private成员变量,改为public会破坏封装,可以用友元,friend关键字修饰函数这些运算符无法重载:.* :: sizeof ?: .
前++与后++的运算符重载
返回类型不同
形参不同
代码不同
效率不同
效率不同 | 前++ | 后++ |
---|---|---|
返回类型不同 | Date& | Date |
– | – | – |
形参不同 | 无 | int |
– | – | – |
代码不同 | 加一直接返回 | 给this+1,返回临时对象 |
– | – | – |
效率不同 | 高 | 低(多了拷贝构造和析构,虽然编译器会优化) |
class Date
{
public:
Date(){
cout <<"构造定义:"<< this << endl;
}
Date(const Date& d)
{
cout <<"拷贝构造:"<< this << endl;
}
void print();
//运算符重载函数
bool operator==(const Date& d)//比较两个Date对象,只用传一个参数,另一参数隐式通过this传递
{
return d._day == this->_day &&
d._month == this->_month &&
d._day == this->_day;
}
//检测两个日期类型对象是否相等
Date& operator=( const Date& d)//赋值运算符重载,只能重载成类的成员函数
{
this->_year = d._year;
this->_month = d._month;
this->_day = d._day;
return *this;
}
Date operator++()//前加加
{
Date temp(*this);
this->_day++;
return temp;
}
Date& operator++(int)//后加加
{
this->_day++;
return *this;
}
private:
int _year;
int _month;
int _day;
};
void Date::print()
{
cout << this->_year << "/" << this->_month << "/" << this->_day << endl;
}
int main(){
Date d1;//构造定义:005DFA90
d1.print();
Date d2(d1);//拷贝构造:005DFA7C
d2.print();
Date d3;//构造定义:005DFA68
d3.print();
d3 = d2;//这一步是赋值
if (d3 == d2)//二进制“==”: 没有找到接受“Date”类型的左操作数的运算符(或没有可接受的转换)
{ cout << "d3和d2相等" << endl; }
//bool operator==(const Date& d);//有了重载运算符定义后,就可以d3==d2这样用
Date d4 = d2;//拷贝构造:005DFA54,这里=是运算符重载,拷贝构造函数
d4++.print();//后加加
(++d4).print();
d4.print();
}
const成员函数
思考:
普通对象可以调用const成员函数,const只能调用const成员函数(1,2)
非const成员函数可以调用其他const成员函数,const成员函数不可调用非const(3,4)
const调用非const,会不安全因素
5.6.取地址及const取地址操作符重载
class Date
{
public :
Date* operator&()
{
return this ;
}
const Date* operator&()const//返回值为this,因为函数被const修饰,实际修饰this,所以在Date*前要加上const
{
return this ;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容
取地址运算符 & 重载的函数原型
Date* operator&();
const Date* operator&()const;
#include<iostream>
using namespace std;
class Date
{
public:
Date(){};
~Date(){};
Date(int y, int m, int d);
Date* operator&()//取地址&运算符重载
{
cout <<"Date* const"<< this << endl;
return this;//this的类型 Date*const
}
const Date* operator&(int)const
{
//这是位与重载,因为取地址是单目运算符
}
const Date* operator&()const
//与第一个重载函数,并不会重载冲突,
//因为这个函数的隐式参数是const Date* const与Date* const不同,
//参数不同,不会发生重载冲突
{
cout << "const Date* const"<<this << endl;
return this;//this的类型const Date*const
}
private:
int year;
int month;
int day;
};
Date::Date(int y, int m, int d)
{
year = y;
month = m;
day = d;
}
int main()
{
Date d1(2022, 11, 19);
//在对象取地址的同时需要将对象的地址打印出来
Date* p = &d1;//有打印说明调用了运算符重载
const Date d2;
const Date* cp = &d2;
return 0;
}
初始化列表:
构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。而有些成员必须要在定义时初始化,这时就要用初始化列表。
必须放在初始化列表位置进行初始化:
引用成员变量
const成员变量
自定义类型成员(且该类没有默认构造函数时)
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
class Time
{
public:
Time();
~Time();
private:
int hour;
int minutes;
int second;
};
Time::Time()
:hour(11),
minutes(11),
second(11)
{
cout <<"time:"<< hour << ":" << minutes << ":" << second << endl;
}
Time::~Time()
{
}
class Date
{
public:
Date(int y = 1900, int m = 1, int d = 1)
:year(y),
month(m),
day(d),
//用初识化列表,初识化顺序,为类中声明时的顺序
_a(1),
_rday(day),
t1()//对t1的初始化采用t1的构造方法
{
//构造方法并不是对象的初始化,初始化只能一次赋值,构造方法中可以多次赋值
cout <<"date:"<< year << "年" << month << "月" << day << "日" << endl;
}
~Date()
{
cout << "~Date" << this << endl;
}
private:
int year;
int month;
int day;
//定义时需要初始化,这时就需要用到初始化列表
/*
必须放在初始化列表位置进行初始化:
引用成员变量
const成员变量
自定义类型成员(且该类没有默认构造函数时)
*/
const int _a;
int& _rday;
Time t1;
};
/*
使用初始化列表初始化,无论是否使用初始化列表,
对于自定义类型成员变量,一定会先使用初始化列表进行初始化
1.初始化列表可以不写,不写不代表编译器不执行
2.如果不写,对于内置类型的成员变量编译器用随机值初始化
对于自定义类型的成员变量,编译器调用无参或者全缺省的构造方法进行初始化
如果累没有无参或者全缺省的构造方法,则报错
*/
int main()
{
Date d1(1,2,3);
}
explicit关键字
用explicit修饰构造函数,将会禁止构造函数的隐式转换
#include<iostream>
using namespace std;
class Date
{
public:
Date();
~Date();
//explicit:限制构造函数不可进行隐式类型转换
Date(int y = 1900, int m = 1, int d = 1)
{
year = y;
month = m;
day = d;
count++;
cout << "Date::count=" << count << endl;
cout << "Date(int,int,int)" << this << endl;
}
static int getCount()//无this指针,this只能用于非静态成员函数
{
return count;
}
private:
int year;
int month;
int day;
static int count;//类中声明,静态成员,不在类外定义会报链接错误,说明没有定义
//没有存储在具体的对象中,但可以通过对象访问
};
int Date::count = 0;//类外定义
Date::Date()
{
}
Date::~Date()
{
cout << "~Date:" << this << endl;
}
int main()
{
Date d1(2022);
d1 = 2023;//调用单参构造方法生成一个匿名对象,然后进行赋值运算符重载
//这样运行是可以的,但是可读性不高,如不了解内部执行逻辑,会有很大的理解偏差,可以用explicit
cout <<Date::getCount() << endl;
return 0;
}
static成员:
用static修饰的成员变量称为静态成员变量,修饰成员函数,称为静态成员函数。
静态成员变量要在类外进行初始化
特性:静态成员为所有类对象共享,不属于某个具体对象,放在静态区,
在类外定义(定义是不加static),类中只是声明
访问 类名::静态成员 对象.静态成员
静态成员函数,没有隐藏的this指针,不能访问呢非静态成员
静态成员受访问限定符的限制
友元:
友元提供了一种突破封装的方式,有时提供了方便,但会增加耦合,破坏封装,不宜多用(封装、继承、多态面向对象三大特性)
友元函数:
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字
友元不受访问限定符的限制,不能加const修饰,不是类的成员函数,但可以访问类的成员变量
友元类:
单向的朋友
Date是Time的友元可以访问其内容,但是Time不能访问Date
不能传递(朋友的朋友不是朋友)
无法继承
内部类:
如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中
的所有成员。但是外部类不是内部类的友元。
特性:
- 内部类可以定义在外部类的public、protected、private都是可以的。
- 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
- sizeof(外部类)=外部类,和内部类没有任何关系。
总结:
类是对某一类实体(对象)来进行描述的,描述该对象具有那些属性,那些方法,描述完成后就形成了一种新的自定义类型,才用该自定义类型就可以实例化具体的对象