文章目录
一、类的引入和定义
1.引入
C++当中定义类有两个关键词struct
和class
struct student
{
char name[20];
int age;
};
class teacher
{
char name[20];
int age;
};
C++ 兼容 C 中结构体的用法,同时 struct 在 C++ 中也升级成了类
并且可以直接使用student来定义变量,不需要typedef
2. 类的定义
C++中的 struct(类)和结构体不同的是:除了可以定义成员变量(变量)还可以成员函数(函数)
对于一个类来说,类体当中有成员变量
和成员函数
接下来以日期类为例子
class Date
{
public:
Date(int year=1970,int month=1,int day=1 )
:_year(year),
_month(month),
_day(day)
{}
~Date()
{
_year = _month = _day = 0;
}
private:
int _year;
int _month;
int _day;
};
3. 类的两种定义方式
-
声明和定义全部放在类体中,需注意:成员函数如果在
类中定义
,编译器可能会将其当成内联函数
处理
-
类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:
成员函数名前需要加类名::
4. 类的访问限定符及封装
面向对象的三大特性:封装、继承、多态
1. 访问限定符
- public修饰的成员在类外可以直接被访问
- protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
- 如果后面没有访问限定符,作用域就到 } 即类结束。
- class的默认访问权限为private,struct为public(因为struct要兼容C)
访问限定符是约束外面的,对于类中,则没有限定,类里面可以全局访问
2. 封装
在C中,数据和函数是分开的,没有严格的封装机制。C++通过类的概念实现了数据和方法的封装,将它们放在一个统一的单元中。
在C++中,类提供了访问控制机制,可以限制对类中数据成员的直接访问。通过使用访问修饰符(public、private、protected),可以控制哪些成员可以被外部访问,哪些成员只能在类内部访问。这样可以保护数据的完整性和安全性,避免了对数据的非法操作。
5. 类的作用域
类定义了一个新的作用域 在类的外面定义成员时,需要使用作用域操作符::
去指明属于哪一个类
Date::Date(int year, int month, int day)
:_year(year),
_month(month),
_day(day)
{}
6. 类的实例化
用类类型创建对象的过程,称为类的实例化
类是对 对象 进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它 。
7. 计算类的大小
计算类的大小和C语言当中的结构体内存对齐是相似的
class Date
{
public:
Date(int year=1970,int month=1,int day=1 )
:_year(year),
_month(month),
_day(day)
{}
~Date()
{
_year = _month = _day = 0;
}
private:
int _year;
int _month;
int _day;
};
class test
{
void fun() {
;
}
};
class info
{
private:
const int a;
};
对于空类和只有成员函数的类,他们按理来说是不占据空间的
但是怎么可能呢,还是得占据一个字节用来占位的
还有就是对于成员函数来说,相当于成员函数是放在代码公共区的
如果每个对象被创造的话都需要属于自己的成员函数,那么会特别浪费空间的
对于const 类型来说也是一样的,都是放在公共代码区
8 . this指针
C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。
二、类的六个默认成员函数
1. 构造函数
对于日期类来说,可以使用全缺省的构造函数
Date(int year=1970,int month=1,int day=1 )
:_year(year),
_month(month),
_day(day)
{}
默认构造函数
1.没写构造函数,编译器自带的默认构造
2.不带参数的默认构造
Date()
{ }
3.全缺省的默认构造
Date(int year=1970,int month=1,int day=1 )
:_year(year),
_month(month),
_day(day)
{}
注意
对于内置类型来说,编译器不会去初始化
对于内置类型来说,编译器会去调用他的默认构造函数(即使没有写)
class AA
{
public:
AA(int a = 1, int b = 2)
{
_a = a;
_b = b;
}
private:
int _a;
int _b;
};
class test
{
public:
test()
{
}
private:
int t;
AA aa;
};
int main()
{
test tt;
return 0;
}
初始化列表
- 初始化列表主要用于对类的成员变量进行初始化。
- 它是在构造函数的定义和函数体之间使用冒号(:)进行设置。
- 并且初始化列表是按照声明的顺序进行初始化。
- 初始化列表是成员变量整体定义的地方。
先来看一个代码示例
之前我们用两个栈实现了一个队列。那么假设现在的栈Stack当中没有默认构造呢
class Stack
{
public:
Stack(int capacity)
{
_a = (int*)malloc(sizeof(int) * capacity);
_top = 0;
_capacity = capacity;
}
private:
int* _a;
int _top;
int _capacity;
};
class MyQueue
{
public:
MyQueue()
{
}
Stack st1;
Stack st2;
};
那么这里会报类Stack当中没有默认构造函数
那么此时就需要出手初始化列表出手
MyQueue()
:st1(4),
st2(4)
{
}
对于初始化列表中没有显式写出初始化项的情况
-
内置类型(如int、float等):
- 没有写初始化列表:
编译器不会进行任何处理
,这些内置类型的值是未定义的。
- 没有写初始化列表:
-
自定义类型:
- 没有写初始化列表:
a.存在默认构造函数
:编译器会调用该类型的默认构造函数来初始化对象
b.不存在默认构造函数或不可访问
:在没有显式初始化列表的情况下,编译器将会报错
。
- 没有写初始化列表:
class A
{
public:
A()
{ _a = 1;}
private:
int _a;
}
class test
{
public:
test()
{ }
private:
int r;
A a;
};
还有就是对于const和引用类型的成员,需要在初始化列表定义
class A
{
public:
A()
:_a(1),
_b(2),
c(_a)
{ }
private:
int _a;
const int _b;
int& c;
};
explicit关键字
隐式类型转换:
代码中发生了隐式类型转换
对于A a来说,先拿3去构造了一个临时对象A tmp(3), 然后tmp再拷贝构造给a
对于Bb来说,先拿{1,2}去构造了一个临时对象B tmp({1,2}), 然后tmp再拷贝构造给b
但是 C++ 编译器在连续构造的过程中,多个构造会被优化,合二为一,变为一个构造
也就是3直接构造a,{1,2}直接构造b
如果不想发生隐式类型转换的话,可以在构造方法前面加 explicit关键字
2 . 析构函数
析构函数,是和构造函数相反功能的一个函数
对象在销毁时会自动调用析构函数,完成对象中资源的清理工作
析构函数:~classname()
class Stack
{
public:
Stack()
:_a(nullptr),
_top(0),
_capacity(4)
{
_a = (int*)malloc(sizeof(int) * 4);
}
~Stack()
{
free(_a);
_top = _capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
对于栈stack来说,以前我们需要在程序结束的时候去free释放我们在堆上开辟的空间
避免内存泄漏
我们实例化Stack之后,程序结束,编译器会默认的调用析构函数~Stack()去释放堆上的空间
注意
假如我们没有写析构函数,那么编译器不会对内置类型去处理析构,对于自定义类型的哈,编译器会去调用他的析构函数
class Stack
{
public:
Stack()
:_a(nullptr),
_top(0),
_capacity(4)
{
_a = (int*)malloc(sizeof(int) * 4);
}
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
class test
{
public:
private:
int size;
Stack st;
};
析构前
析构后
3. 拷贝构造函数
拷贝构造函数是构造函数的一个重载
对栈Stack的拷贝构造来说
(深拷贝)
Stack(const Stack& s)
{
_a = (int*)malloc(sizeof(int) * s._capacity);
_top = s._top;
_capacity = s._capacity;
for (int i = 0; i < s._top; i++)
_a[i] = s._a[i];
}
对Date的拷贝构造来说
Date( const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
调用拷贝构造的时候,不能传值,因为第一步是传参,然而他会认为传参就是调用拷贝构造,然后又要传参,又要调用拷贝构造,无穷递归了
注意
注意,如果我们没有写拷贝构造函数的话,编译器的默认拷贝构造都是浅拷贝
拿Stack来说,也就是将Stack当中的_a, _top, _capacity
直接给另外一个
这样子的话就是两个栈指向了同一块堆上的空间,那么这样子就析构两次,出现free野指针的问题了,会崩溃
但是会有疑问,不是free指针之后,将_a指向的空间赋值为nullptr了嘛,但是需要注意的是
st2析构的时候,虽然将_a指向的空间赋值为nullptr了,st当中的_a指向的空间也被释放了,但是st当中的_a还是野指针(可以理解为形参)
由此,我们总结出一个结论:当自己实现了析构函数释放空间,就需要实现拷贝构造(深拷贝),而深拷贝我们之后也会详细讲解。
拷贝构造函数典型调用场景:
1、使用已存在对象创建新对象
2、函数参数类型为类类型对象
3、函数返回值类型为类类型对象
4、 运算符重载
规则
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
在 C++ 中,只支持对内置类型的运算符计算,并不支持直接对自定义类型进行运算符计算,但是通过运算符重载可以达到这个效果
对于一个日期来说,我们想知道这个日期加上一定的天数之后,新的日期是多少
为了更加方便,增加代码的可读性,我们可以重载运算符+=
Date& operator+=(long long day)
{
_day += day;
while (_day > GetMonDay(_year, _month))
{
_day -= GetMonDay(_year, _month);
_month++;
if (_month == 13)
{
_year++;
_month = 1;
}
}
return *this;
}
运算符重载:
函数名字为:关键字operator后面接 需要重载的运算符符号
函数原型:返回值类型 operator操作符(参数列表)
返回值与参数:
对于返回值:不同的运算符重载函数,返回值是不同的,例如 > 就是 bool 类型;- 就是 int 类型
对于参数操作数有几个操作符,就有几个操作数
赋值运算符重载
赋值运算符重载是对于两个已经存在对象之间的赋值拷贝
这里的赋值运算符重载–深赋值
void operator=(const Stack& st)
{
_a = (int*)malloc(sizeof(int) * st._capacity);
_top = st._top;
_capacity = st._capacity;
for (int i = 0; i < _capacity; i++)
_a[i] = st._a[i];
}
5、const 成员
若定义了一个 const 的对象,然后访问其成员函数,会发生什么情况
对于 Date d
,传递过去的 &d 是 Date*
;而 const Date d2
,传递过去的 &d2 是 const Date*
.
对于const的对象,不能去调用非const的成成员函数,不然就是权限的放大
对于非const 的对象,可以调用非const的成员函数和const的成员函数
void print()
{
cout << _year << " " << _month << " " << _day << endl;
}
void print()const
{
cout << _year << " " << _month << " " << _day << endl;
}
6 . 取地址与const取地址操作符重载
我们知道,对于自定义类型成员来说,平常的操作符需要重载后才能对对象进行操作。但是对于自定义类型的对象来说,如果不写这两个成员函数,使用默认的成员函数照样也可以完成目的:
所以我们一般不写,但是写的话也可以:
class A
{
public:
A* operator&()
{
return this;
}
const A* operator&() const
{
return this;
}
};
就只要返回 this 就可以;对于 const 取地址操作符,则要加上 const 成员,并且返回的指针也要加上 const 修饰
三、类的拓展点
1、static 成员
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化
特性
- 静态成员变量是属于整个类而非具体对象的成员,存放在静态区,且在内存中只有一份拷贝。
- 静态成员变量需要在类外进行定义,类中只需声明。
- 可以使用类名加作用域解析运算符或对象名来访问静态成员变量。
- 静态成员函数没有隐含的this指针,无法访问非静态成员变量或非静态成员函数。
- 静态成员函数只能访问静态成员变量和调用其他静态成员函数。
- 静态成员受到访问限定符的控制,可以根据需要限制对静态成员的访问权限。
总结:静态成员是类的成员,具有特殊的共享和访问方式。静态成员变量存放在静态区,静态成员函数没有this指针,只能访问静态成员。静态成员受到访问限定符的控制,可以根据需要限制对静态成员的访问权限。
class A
{
public:
static int st;
A(int a=1)
{
_a = a;
}
private:
int _a;
};
int main()
{
A::st = 1;
A a = 3;
return 0;
}
2 . 友元
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元分为:友元函数和友元类
1. 友元函数
对于日期类,我们想要重载一下流插入和流提取运算符号
那我们可以这样子
istream operator(istream& cin)
{
cin >> _year >> _month >> _day;
return cin;
}
ostream& operator<<(ostream& cout)
{
cout << _year << " " << _month << " " << _day;
return cout;
}
但是我们在使用流提取和流插入的时候,就要d<<cout
和d>>cin
不怎么符合习惯,那么就就不要再Date当中重载,不然的话第一个参数都是this指针
那如果是cout<<d
呢,那么传过去的this指针即使cout的指针,函数当中就没有了Date d了
那么就可以使用友元函数了,这样子就可以访问到private修饰的成员变量
friend istream& operator>>(istream& cin, Date& d)
{
cin >> d._year >> d._month >> d._day;
return cin;
}
friend ostream& operator<<(ostream& cout, const Date& d)
{
cout << d._year << " " << d._month << " " << d._day;
return cout;
}
总结:
1.友元函数 不能用const修饰
2.友元函数可以在 类定义的任何地方声明,不受类访问限定符限制
2. 友元类
友元类的所有成员函数都变为是另一个类的友元函数,都可以访问另一个类中的非公有成员
友元关系是单向的,不具有交换性
class Time
{
friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
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;
};
Date类可以直接访问Time类当中的成员
对于Time类来说,不可以访问Date类的成员
3. 内部类
概念:
-
内部类是定义在另一个类的内部的独立类,它不属于外部类
-
外部类没有特殊的访问权限来访问内部类的成员
-
内部类可以访问外部类的成员,包括私有成员,
内部类是外部类的天生友元类
内部类 B 天生就是外部类 A 的友元,也就是 B 中可以访问 A 的私有,A 不能访问 B 的私有 :就比如 B 类中可以访问 A 中私有成员变量,但是 A 不能访问 B 的私有,除非将权限开放为公有
class Solution {
private:
class Sum
{
public:
Sum()
{
_ret += _i;
++_i;
}
};
private:
static int _i;
static int _ret;
public:
int Sum_Solution(int n) {
Sum a[n];
return _ret;
}
};
int Solution::_i = 1;
int Solution::_ret = 0;
4 . 匿名对象
在C++中,匿名对象是指在创建对象时不给对象命名,直接将其作为表达式的值使用,而不将其赋值给变量
匿名对象通常用于简化代码和临时操作
-
匿名对象可以在需要一个临时对象的地方使用,例如作为函数参数或返回值
-
匿名对象的生命周期仅限于当前的语句块,超出当前语句块它将被销毁
-
匿名对象可以调用对象的成员函数和访问成员变量,就像一般的具名对象一样
-
加了const修饰,匿名对象的生命周期会延长
不加const
加了const
四、总结
类和对象这块的知识点相对来说是比较多的,但是需要好好理解
类的定义,六大默认成员函数,以及拓展点
类是对某一类实体(对象)来进行描述的,描述该对象具有哪些属性,哪些方法,描述完成后就形成了一种新的自定义类型,才用该自定义类型就可以实例化具体的对象