类与对象:
- 类的介绍:
- 1.类的简单介绍
- 2.类的作用域
- 3.类的访问限定符及封装
- 访问限定符:
- 封装:
- 4.类的对象大小的计算
- 5.类成员函数的this指针
- this指针的特性
- 类的默认成员函数:
- 1. 类的6个默认成员函数
- 2. 构造函数
- ①功能:完成对象的初始化
- ②特性及用法:
- ③编译器默认生成
- 默认构造函数:
- 3. 析构函数
- ①功能:完成类的一些资源清理工作
- ②特性及用法:
- ③编译器默认生成
- 4. 拷贝构造函数
- ①功能:完成对象成员变量的拷贝(浅拷贝)
- ②特性及用法:
- ③编译器默认生成
- 5. 赋值运算符重载
- ①功能:为了增强代码的可读性
- ②特性及用法:
- ③编译器默认生成
- 6.取地址操作符重载
- 7.const修饰的取地址操作符的重载
- 类的其他特性:
- 1. const成员函数
- 2. 友元
- 友元函数
- 友元类
- 3.再谈构造函数
- 构造函数体赋值
- 初始化列表
- 4. static成员
类的介绍:
1.类的简单介绍
class className{
// 类体:由成员函数和成员变量组成
}; // 一定要注意后面的分号
class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号。
类中的元素称为类的成员,类中的数据称为类的属性或者成员变量; 类中的函数称为类的方法或者成员函数
2.类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中。
在类体外定义成员,需要使用 :: 作用域解析符 指明成员属于哪个类域。
示例:
#include<iostream>
using namespace std;
class Person
{
public:
//声明
void show();
private:
static int age;
};
void Person::show()
{
cout << age << endl;
}
int Person::age = 18;
int main()
{
Person p;
p.show();
return 0;
}
可以看到我们能在Person类外定义show函数以及定义静态变量age
3.类的访问限定符及封装
访问限定符:
通过访问限定符对类的成员变量或成员函数进行封装,这样能有效保护类中的数据,或者隐藏类中方法实现的细节
访问限定符的说明:
-
public修饰的成员在类外可以直接被访问
-
protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
-
访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
-
class的默认访问权限为private,struct为public(因为struct要兼容C)
封装:
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节 ,仅对外公开接口来和对象进行交互。
封装本质上是一种管理:
将一些公共的,想让人们访问的设为public
让不想被访问的设置为private或protected
4.类的对象大小的计算
#include<iostream>
using namespace std;
class Person
{
public:
void show();
private:
static int age;
int num;
};
void Person::show()
{
cout << age << endl;
}
int Person::age = 18;
int main()
{
Person p;
cout << sizeof(Person) << endl;
return 0;
}
可以看出类的大小仅为4
那是怎么计算的呢?
类中的成员函数并不储存在类中,而在公共区域(代码段),同样静态成员也不在类中,而在静态常量区:
因为在不同的对象中,调用相同的函数都是同一个函数,为节约空间,就把它存放在公共区域中,这样实例化不同的对象,调用函数都共用一个函数空间。
而静态成员是并不属于某一个对象,属于类的属性,每个对象都共用一个静态成员,所以静态成员并不在对象中,不会计入大小
所以只计算非静态成员变量的大小,按结构体对齐原则来确定最终大小
注意:
当为空类时,大小为1
#include<iostream>
using namespace std;
class Person
{};
int main()
{
Person p1;
Person p2;
cout << sizeof(Person) << endl;
cout << sizeof(p1) << endl;
cout << sizeof(p2) << endl;
return 0;
}
这是为了区分p1对象和p2对象:
实例化不同的对象,为了保证它的不同,所以地址必须不同,用那一个字节站位来区别不同的对象
5.类成员函数的this指针
上面提到每个对象共用一个成员函数,那编译器是怎么区分他们各自的属性的呢
在调用成员函数时,编译器会自动转化来实现操作某一个特定对象
#include<iostream>
using namespace std;
class Person
{
public:
void showage(int age)//编译器自动转化为void showage(Person*this,int age)
{
m_age = age;//this->m_age=age;
cout << m_age << endl;//cout << this->m_age << endl;
}
private:
int m_age;
};
int main()
{
Person p1;
p1.showage(18);//p1.showage(&p1,18);传p1引用
return 0;
}
从上面看出:
1.this指针是隐含的,是编译器编译时自动添加的,调用函数时,我们不能显式地写出来(报错)
2.可以在成员函数中直接使用this指针
this指针的特性
-
只能在“成员函数”的内部使用
-
this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
-
this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递
注意:
(1) this指针存放在寄存器中
(2) this指针可以为空(不需要使用到this),但当为空时,再指向成员变量会报错
类的默认成员函数:
1. 类的6个默认成员函数
2. 构造函数
①功能:完成对象的初始化
②特性及用法:
(1). 函数名与类名相同
(2).无返回值
(3). 对象实例化时编译器自动调用对应的构造函数
(4). 构造函数可以重载。
③编译器默认生成
在编译器默认生成的构造函数中对成员变量:
1.对内置类型(像char,int那种基本类型)不做处理
2.对自定义类型(像class,struct那种自己定义的)会调用其**默认构造函数**初始化
默认构造函数:
1. 我们不写,编译器默认生成的
2. 我们自己写的无参的
3. 我们写的全缺省的
总之不用传参就可以调用的函数就是默认构造函数
示例:
#include<iostream>
using namespace std;
class student
{
public:
student()
{
cout << "调用student默认构造函数" << endl;
}
};
class Person
{
// 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成
public:
//实例化对象是自动调用,保证对象一定初始化
private:
int m_age;//内置类型
student s;//自定义类型
};
int main()
{
Person p1;
return 0;
}
3. 析构函数
①功能:完成类的一些资源清理工作
②特性及用法:
1.析构函数名是在类名前加上字符 ~
2. 无参数无返回值。
3. 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
#include<iostream>
using namespace std;
class Stack
{
public:
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int) * capacity);
_size = 0;
_capacity = capacity;
}
void Push(int x)
{}
// 像对象销毁后需要重新指定某成员变量的值的或者开辟了动态内存需要free的,析构函数就非常重要
~Stack()
{
cout << this<<"~Stack()析构函数" << endl;
// 清理资源
free(_a);
_a = nullptr;
_size = _capacity = 0;
}
private:
int* _a;
int _size;
int _capacity;
};
int main()
{
Stack st1;
st1.Push(1);
st1.Push(2);
st1.Push(3);
cout << "st1:" << &st1 << endl;
Stack st2;
st2.Push(10);
st2.Push(11);
st2.Push(12);
cout << "st2:" << &st2 << endl;
return 0;
}
可以看出:先析构的是后定义的st2,后析构的是st1
因为对象是定义在函数中的,函数的调用会建立栈帧
栈帧的对象构造和析构也要遵守先进后出的原则
③编译器默认生成
同样,对于内置类型的成员变量的不做处理
对于自定义类型的会调用它的析构函数
4. 拷贝构造函数
①功能:完成对象成员变量的拷贝(浅拷贝)
②特性及用法:
构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类对象创建新对象时由编译器自动调用
特性:
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用
那为什么会无限递归呢:
每次传的都要类的本身需要拷贝构造就形成无限递归的形式,所以应该传引用,这样就不会拷贝构造
注意:
这里的拷贝是浅拷贝,拷贝的属性有动态内存,这块内存会被对象的析构函数free两次;
这时需要我们自己实现深拷贝
③编译器默认生成
对内置类型浅拷贝
对自定义类型会调用它的拷贝构造完成拷贝
#include<iostream>
using namespace std;
class s1
{
public:
s1(int a = 1)
{
cout << "s1默认构造函数" << endl;
}
s1(const s1 &s)
{
cout << "s1拷贝构造函数调用" << endl;
a = s.a;
}
int a;
};
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 这个拷贝构造对内置类型会完成浅拷贝,或者值拷贝
// 编译器默认生成赋值运算符跟拷贝构造的特性是一致的
// a、针对内置类型,会完浅拷贝
// b、针对自定义类型,也一样,它会调用他的拷贝构造函数完成拷贝
void Print( )
{
cout << _year << "-" << _month <<"-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
s1 s;
};
int main()
{
Date d1(2020, 5, 26);
Date d2(d1);
Date d3=d2;
d1.Print();
d2.Print();
d3.Print();
return 0;
}
5. 赋值运算符重载
①功能:为了增强代码的可读性
②特性及用法:
函数名字为:关键字operator后面接需要重载的运算符符号
函数原型:返回值类型 operator操作符(参数列表)
注意:
1.不能通过连接其他符号来创建新的操作符:比如operator@
2.重载操作符必须有一个类类型或者枚举类型的操作数
3.用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不能改变其含义
4.作为类成员的重载函数时,其形参有一个默认的形参this
5.".*" 、"::" 、“sizeof” 、"?: “、”." 注意以上5个运算符不能重载
如何区别拷贝调用的是构造函数还是赋值运算符重载函数
以上面的Date类为例
Date d1;
Date d2=d1;//这是调用的拷贝构造函数
Date d3;
d3=d1;//赋值运算符重载函数
二者的区别为:
第一个d2并没有创建,未初始化而将d1拷贝到d2
第二个d3已经创建,并且初始化了,所以是将d1赋值给d3
示例:
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date&d)
{
cout << "调用拷贝构造函数" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print( )
{
cout << _year << "-" << _month <<"-" << _day << endl;
}
Date& operator =(const Date& d) //返回Date&是为了实现多重赋值i=j=k
{
if(this!=&d)//如果是自己给自己赋值直接返回
{
cout << "调用赋值运算符重载函数" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2021,9,29);
Date d2 = d1;//这是调用的拷贝构造函数
Date d3(2021,9,29);
d3 = d1;//赋值运算符重载函数
d1.Print();
d2.Print();
d3.Print();
return 0;
}
③编译器默认生成
对内置类型浅拷贝
对自定义类型会调用它的赋值重载
6.取地址操作符重载
Person * operator&()
{
return this;//返回对象的地址
}
7.const修饰的取地址操作符的重载
const Person * operator&() const
{
return this;//返回const修饰的对象地址
}
类的其他特性:
1. const成员函数
将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改
例如:
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
bool operator==(const Date &d1) const
{
return (this->_day == d1._day) &&
(this->_month == d1._month) &&
(this->_year == d1._year);
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2021, 9, 29);
Date d2(d1);
cout << (d1 == d2 )<< endl;//1
return 0;
}
.比如上面Date类中的重载函数==,我们希望比较他们是否相等,并不希望改变他们的值,
所以都加上const来保护类的值
2. 友元
友元分为:友元函数和友元类友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用
友元函数
声明时需要加friend关键字
说明:
① 友元函数可访问类的私有和保护成员,但不是类的成员函数
② 友元函数不能用const
③ 修饰友元函数可以在类定义的任何地方声明,不受类访问限定符限制
④ 一个函数可以是多个类的友元函数
⑤ 友元函数的调用与普通函数的调用和原理相同
友元类
友元类的所有成员函数都可以是另一个类的友元函数,其还可以访问另一个类中的非公有成员
**特性:
① 友元关系是单向的,不具有交换性:
A类是B类的友元,A类可以访问B类私有成员变量,B类不能访问A类私有成员变量
② 友元关系不能传递:
如果B是A的友元,C是B的友元,则不能说明C时A的友元**
#include<iostream>
using namespace std;
class Date
{
friend class s1;//s1类是Date类的友元,故s1类可以访问Date的私有成员
public:
Date(int year, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void show()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
class s1
{
public:
s1()
:
d(1)
{
a = 0;
}
void set(int year, int month, int day)
{// 直接访问时间类私有的成员变量
d._year = year;
d._month = month;
d._day = day;
}
void show()
{
d.show();
}
private:
int a;
Date d;
};
int main()
{
s1 s;
s.set(2021, 9, 30);
s.show();
return 0;
}
3.再谈构造函数
构造函数体赋值
using namespace std;
class Date {
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
}
构造函数体中的语句只能将其称作为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值
初始化列表
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式
【注意】
-
.每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
-
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
-
类中包含以下成员,必须放在初始化列表位置进行初始化:
⑴ 引用成员变量⑵ const成员变量
⑶ 自定义类型成员(该类没有默认构造函数)
#include <iostream>
#include <string>
#include <vector>
using namespace std;
class A
{
public:
A(int x)
{
cout << "A(int x)" << endl;
_x = x;
}
private:
int _x;
};
class Date
{
friend class Time;
public:
// 初始化列表的主要作用是给必须在定义的时候赋值的变量初始化
Date(int year = 0, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _n(10)
, _ref(year)
, _a(1) // 显示去调用
{
_day = day;
// _n = 10; // 不能在函数体内初始化,必须使用初始化列表初始化
// _ref = year;
}
private:
// 成员变量声明
int _year;
int _month;
int _day;
// 他们必须在定义的时候初始化
const int _n;
int& _ref;
A _a;
};
int main()
{
Date d1;
//Date d2;
return 0;
}
4. static成员
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;
用static修饰的成员函数,称之为静态成员函数。静态的成员变量一定要在类外进行初始化
特性:
-
静态成员为所有类对象所共享,不属于某个具体的对象,它在静态区
-
静态成员变量必须在类外定义,定义时不添加static关键字
-
类静态成员即可用类名::静态成员或者对象.静态成员来访问
-
静态成员函数没有隐藏的this指针,不能访问任何非静态成员
-
静态成员和类的普通成员一样,也有public、protected、private3种访问级别,也可以具有返回值
#include<iostream>
using namespace std;
class A
{
public:
A()
{
cout << "A()" << endl;
++_n;
}
A(const A& a)
{
++_n;
}
// 跟普通成员函数区别:没有this指针,不能访问非静态成员
static int GetN()
{
// 没有this指针
//_a = 1; // this->_a
// f(); // this->f()
return _n;
}
private:
// 这里只是声明,不在构造函数初始化,在类外面全局位置初始化
static int _n; // n是存在静态区,属于整个类,也属于类的所有对象
int _a;
};
// 静态成员变量的定义初始化,特例,不受访问限定符限制,否则就没办法定义初始化了
int A::_n = 0;
int main()
{
// 静态成员,不属于某个对象,突破类域就能访问
A a1;
cout << a1.GetN() << endl;
cout << A().GetN() << endl;
cout << A::GetN() << endl;
return 0;
}