第二章:类和对象
C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。
1.类的引入
C语言中,结构体中只能定义变量;
在C++中,结构体内不仅可以定义变量,也可以定义函数。
struct Student
{
void SetStudentInfo(const char* name, const char* gender, int age)
{
strcpy(_name, name);
strcpy(_gender, gender);
_age = age;
}
void PrintStudentInfo()
{
cout<<_name<<" "<<_gender<<" "<<_age<<endl;
}
char _name[20];
char _gender[3];
int _age;
};
int main()
{
Student s;
s.SetStudentInfo("Allen", "男", 18);
return 0;
}
2.类的定义
class className
{
};
class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号。
类中的元素称为类的成员:类中的数据称为类的属性或者成员变量; 类中的函数称为类的方法或者成员函数。
类的两种定义方法:
①声明和定义全部放在类体中,
需要注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
②声明放在.h文件中,类的定义放在.cpp文件中。
推荐使用第二种定义方法
3.类的访问限定符及封装
3.1 访问限定符
访问限定符:public(公有);protected(保护);private(私有)
-
public被定义为 公有的类成员,可以在任何地方被访问。 -
protected被定义为 受保护的类成员,则可以被其自身以及其子类和父类访问。 -
private被定义为 私有的类成员,则只能被其定义所在的类访问 -
访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止 -
class的默认访问权限为private,struct为public(因为struct要兼容C)
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
【面试题】
C++中struct和class的区别是什么?
C++需要兼容C语言,所以C++中struct既可以当成结构体去使用,又可以用来定义类。
struct和class定义类是一样的,区别是struct的成员默认访问方式是public,class的成员默认访问方式是private。
3.2 封装
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。封装本质上是一种管理。
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
4.类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员,需要使用 :: 作用域解析符指明成员属于哪个类域。
class Person
{
public:
void PrintPersonInfo();
private:
char _name[20];
char _gender[3];
int _age;
};
void Person::PrintPersonInfo()
{
cout<<_name<<" "_gender<<" "<<_age<<endl;
}
5.类的实例化
用类类型创建对象的过程,称为类的实例化
①类只是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它。
②一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量。
类的实例化,就像现实生活中按照建筑设计图建造了一栋房子(也可以建造许多栋),类就是建筑设计图,不占用土地面积,实例化就是实体的房子,占用土地面积
③一个类的大小,实际就是该类中”成员变量”之和(成员函数存放在公共的代码段 ),当然也要进行内存对齐,注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类。
6.this指针
class Date
{
public :
void Display ();
void SetDate(int year , int month , int day);
private :
int _year ;
int _month ;
int _day ;
};
void Date::Display ()
{
cout <<_year<< "-" <<_month << "-"<< _day <<endl;
}
void Date::SetDate(int year , int month , int day)
{
_year = year;
_month = month;
_day = day;
}
int main()
{
Date d1, d2;
d1.SetDate(2022,3,1);
d2.SetDate(2022,3,3);
d1.Display();
d2.Display();
return 0;
}
对于上述类,有这样一个问题,Date类中有SetDate与Display两个成员函数,函数体中没有关于不同对象的区分,那当s1调用SetDate函数时,该函数是如何知道应该设置s1对象,而不是设置s2对象呢?
C++中通过引入this指针解决该问题,即:C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
void Date::Display ()
{
cout << this->_year<< "-" << this->_month << "-"<< this->_day <<endl;
}
this指针的特性:
this指针存在哪里?
其实编译器在生成程序时加入了获取对象首地址的相关代码。并把获取的首地址存放在了寄存器ECX中(VS编译器是放在ECX中,其它编译器有可能不同)。也就是成员函数的其它参数正常都是存放在栈中。
而this指针参数则是存放在寄存器中。类的静态成员函数因为没有this指针这个参数,所以类的静态成员函数也就无法调用类的非静态成员变量
this指针可以为空吗?
可以为空。
当我们调用函数时,如果函数内部不需要使用到this,也就是不需要通过this指向当前对象并对其进行操作时才可以为空(当我们在其中什么都不放或者在里面随便打印一个字符串)。
如果调用的函数需要指向当前对象,并进行操作,则会发生错误(空指针引用)就跟C中一样不能进行空指针的引用。
7.类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情况下,都会自动生成6个默认成员函数 ,即使这六个成员函数什么也不做。
class A
{
public:
A();
~A();
A(const A& a);
A& operator=(const A& a);
A* operator &();
const A* operator &() const;
};
7.1 构造函数
构造函数:一个特殊的成员函数,名字与类名相同,创建类类型对象的时候,由编译器自动调用,在对象的生命周期内只且调用一次,以保证每个数据成员都有一个合适的初始值。需要注意的是,构造函数的虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
构造函数的特性:
-
函数名与类名相同。 -
无返回值。 -
对象实例化时编译器自动调用对应的构造函数,且在对象的生命周期内仅调用一次。 -
构造函数可以重载,实参决定了调用哪个构造函数。 -
如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。 -
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。 -
构造函数不能用const修饰。(因为const修饰类的成员函数时,该函数不能修改成员变量,但是构造函数要修改类的成员变量,因此不可以由const修饰)
构造函数细节很多,大多数情况需要自己写,推荐写全缺省的构造函数,可以适应多种场景。
构造函数的初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
calss Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
}
注意:
①每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
②类中包含以下成员,必须放在初始化列表位置进行初始化:
引用成员变量;const成员变量;自定义类型成员(该类没有默认构造函数)
【可以理解为,一个对象的单个成员变量在初始化列表是它的定义阶段】
③尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
④成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
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(2018);
d1 = 2019;
}
7.2 析构函数
析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。
析构函数的特性:
7.3 拷贝构造函数
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。拷贝构造函数是特殊的构造函数,创建对象时使用已存在的同类对象来进行初始化,由编译器自动调用。
拷贝构造函数的特性:
7.4 赋值运算符重载
7.4.1 运算符重载
自定义类型默认不支持运算符,C++可以用运算符重载来让类对象支持某个运算符。
运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
-
不能通过连接其他符号来创建新的操作符:比如operator@ -
重载操作符必须有一个类类型或者枚举类型的操作数 -
用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不 能改变其含义 -
作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的 -
操作符有一个默认的形参this,限定为第一个形参 -
【.(成员指针访问运算符) 、::(域运算符) 、sizeof(长度运算符) 、?:(条件运算符) 、.*(成员访问运算符)】以上5个运算符不能重载。这个经常在笔试选择题中出现。
7.4.2 赋值运算符重载
对于类类型的对象我们需要对‘=’重载,以完成类类型对象之间的赋值。
class Date {
public:
Date(int year = 2000, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}
Date& operator=(const Date& d) {
if(this != &d){
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
void show() {
cout << _year << "---" << _month << "---" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 2, 22);
Date d2;
d1.show();
d2.show();
d2 = d1;
d2.show();
return 0;
}
7.5 取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!
class Date
{
public :
Date* operator&()
{
return this ;
}
const Date* operator&() const
{
return this ;
}
private :
int _year ;
int _month ;
int _day ;
};
8.static成员
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态的成员变量一定要在类外进行初始化。
【面试题】
实现一个类,计算中程序中创建出了多少个类对象。
class A
{
public:
A() {++_scount;}
A(const A& t) {++_scount;}
static int GetACount() { return _scount;}
private:
static int _scount;
};
int A::_scount = 0;
void TestA()
{
cout<<A::GetACount()<<endl;
A a1, a2;
A a3(a1);
cout<<A::GetACount()<<endl;
}
特性:
-
静态成员为所有类对象所共享,不属于某个具体的实例 -
静态成员变量必须在类外定义,定义时不添加static关键字 -
类静态成员即可用类名::静态成员或者对象.静态成员来访问 -
静态成员函数没有隐藏的this指针,不能访问任何非静态成员 -
静态成员和类的普通成员一样,也有public、protected、private3种访问级别,也可以具有返回值
9.c++11的成员初始化新玩法
C++11支持非静态成员变量在声明时进行初始化赋值,但是要注意这里不是初始化,这里是给声明的成员变量缺省值。
class B
{
public:
B(int b = 0)
:_b(b)
{}
int _b;
};
class A
{
public:
void Print()
{
cout << a << endl;
cout << b._b<< endl;
cout << p << endl;
}
private:
int a = 10;
B b = 20;
int* p = (int*)malloc(4);
static int n;
};
int A::n = 10;
int main()
{
A a;
a.Print();
return 0;
}
10.友元
友元分为:友元函数和友元类
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用
10.1 友元函数
如果我们尝试去重载operator<<,我们就会发现我们没办法将operator<<重载成成员函数。
因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。
所以我们要将operator<<重载成全局函数。但是这样的话,又会导致类外没办法访问成员,那么这里就需要友元来解决。operator>>同理。
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
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;
}
istream& operator>>(istream& _cin, Date& d)
{
_cin>>d._year;
_cin>>d._month;
_cin>>d._day;
return _cin;
}
int main()
{
Date d;
cin>>d;
cout<<d<<endl;
return 0;
}
说明:
10.2 友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time。
类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
如果B是A的友元,C是B的友元,则不能说明C时A的友元。
class Date;
class Time
{
friend class Date;
public:
Time(int hour = 0, int minute = 0, int second = 0)
: _hour(hour)
, _minute(minute)
, _second(second)
{}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
void SetTimeOfDate(int hour, int minute, int second)
{
_t._hour = hour;
_t._minute = minute;
_t.second = second;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
11.内部类
概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。注意此时这个内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去调用内部类。外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的友元类。注意友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
特性:
-
内部类可以定义在外部类的public、protected、private都是可以的。 -
注意内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名。 -
sizeof(外部类)=外部类,和内部类没有任何关系
class A
{
private:
static int k;
int h;
public:
class B
{
public:
void foo(const A& a)
{
cout << k << endl;
cout << a.h << endl;
}
};
};
int A::k = 1;
int main()
{
A::B b;
b.foo(A());
return 0;
}
|