目录
一.类的定义
class name
{
//类体:成员函数和成员对象
};
1.定义方式
1.声明和定义放在类内,对于成员函数如果放在类内定义,编译器可能会将其当为内联函数处理。
2.声明和定义分离
2.类的访问限定符
1.public:修饰的成员可以在类外直接访问。
2.protected和private修饰的成员不可在类外直接访问。
3.class默认访问权限为private,struct是public(为了兼容c语言)。
3.类的封装
将成员函数和成员对象分离,对外公开则用public,否则用protected(private)
4.类的作用域
类中是一个新的作用域,叫做类域,因此类的所有成员都在类内的作用域中。对于类外的成员,要用作用域解析符“::"来指明成员属于哪个类
class Date
{
public:
//。。。。
print()
{
}
private:
//。。。。
};
//这里需要指定ShowInfo是属于Person这个类域
void Date::print()
{
//。。。。。
}
5.类的实例化(创建对象)
用类这个自定义类型创建对象的过程。
在定义类时,只是说明了类有哪些成员,(定义一个类),
并没有为类分配一个实际的内存空间。
6.类对象的大小(sizeof)
一个类的大小,是成员变量大小之和,(存在内存对齐)。
注意对于空类实例化的对象的大小,空类的大小为1,因为编译器给空类一个字节来表示这个类实例化的对象,(可以认为这个类实例化的对象存在的证明)(也就是占位)。
至于为什么没有计算成员函数,因为如果计算了成员函数,每个对象中的成员函数都是相同的,造成了空间的浪费。
成员函数放在公共的代码段中。
7.结构体内存对齐规则
对齐数=成员变量本身大小和编译器默认对其数的较小值。
1.第一个成员在结构体首地址处,对齐到0处)
2.其他成员要对齐到(对齐数)的整数倍的地址处。
3.结构体的大小即最大对齐数的整数倍。
4.如果含有结构体,同其他类型(对齐到这个结构体的最大对齐数,就不是编译器的较小值了)的整数倍。
内存对齐原因
1.平台原因(移植原因)。有些平台取数据时是取数据的整数倍,没有内存对齐就会出现异常。
2.性能原因:对于未对齐的内存,处理器要进行两次内存访问(可能),而对齐的内存只需要一次(可能)。
内存对齐本质是:空间换时间。
修改编译器默认对齐数:
#pragma pack(4)//设置默认对齐数为4
8.this指针
用处:
Date d;
d.print();
由于成员函数放在公共的代码段中。将类可以实例化出来的不同对象,调用各自的非静态成员函数时,该函数为了知道是哪个对象调用的,该非静态成员函数会有有个隐藏的this指针,指向该函数的对象,是编译器自动调用的。那么这个处于公共代码段非静态成员函数就可以通过指向某个对象的this指针指到是哪个对象调用的了(从而实现对每个对象的成员变量的修改)。
this指针的特性
1.类型:类的类型*const (this指针不可改,指向的内容可改)。
2.只能在非静态成员函数中使用,静态成员函数没有this指针。
3.本质是成员函数的第一个形参(隐含的)。
一道有意思的代码
#include <iostream>
using namespace std;
class A
{
public:
void PrintA()
{
cout << _a << endl;
}
void Show()
{
cout << "Show()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
//p->Show(); //第一句代码
//p->PrintA(); //第二句代码
}
运行第一行代码时,虽然p是空指针,但是它在show函数中是作为形参传到了隐藏的this指针中,而这个this指针在这个函数中并没有解引用,所以程序不会崩溃。
而对于print()函数,由于里面有对this形参的解引用,就会崩溃。
void PrintA()
{
cout << this->_a << endl;
}
void Show()
{
cout << "Show()" << endl;
}
二.类的六大默认成员函数
默认的其中一个含义:你不显式写,系统会默认生成。(只是其中一个含义会面会讲其他含义)
c++11声明时给缺省值
注意这不是初始化
class Date
{
//对象实例化必须要有构造函数
public:
void Print()
{
cout << _day << "/" << _month << "/" << _year<<endl;
}
int _day=1;//声明时给缺省值
int _month=2;
int _year=3;
A _aa;
};
如果初始化列表有初始化,那么就不用声明给的缺省值,
初始化列表没有初始化,那么就用声明时给的缺省值,
如果声明时也没有给缺省值,那么就是随机值了
1.构造函数
1.目的
保证每一个成员变量有一个初始值。
2.特性
1. 函数名与类名相同。
2. 无返回值。
3. 对象实例化时编译器自动调用对应的构造函数,类实例化时必须要有构造函数
4. 构造函数可以重载
5. 没有显式定义构造函数 则 编译器会生成一个无参的默认构造函数,如果有显式定义 编译器不在生成
6. 系统默认生成的构造函数对自定义类型调用自己的构造函数,内置类型不做处理
类型分为
1.内置类型(int)
2.自定义类型(class struct )
Date d1; 默认构造无参
Date d2();无参不能加括号 错误!无法区分函数声明
3.默认构造函数
要区分系统默认生成的默认构造函数,只是其中之一
4.default
使用场景
类名:Date()=default;强制编译器生成构造函数
即不用传参的构造函数
1如不显式定义的系统自动生成过的
2全缺省的构造函数(推荐)
3无参的构造函数
5.初始化列表中的初始化顺序
成员变量在类中声明次序就是其在声明的顺序,与其在初始化列表中的先后次序无关
输出1 随机值
class A
{
public:
A(int a)
:_a1(a)//后::a1被a初始化
, _a2(_a1)//先::a2被a1随机值初始化,
{}
void Print() {
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2;、//先初始化a2 再a1
int _a1;
};
int main() {
A aa(1);
aa.Print();
}
6.初始化列表初始化作用
(以下必须初始化列表初始化)
1引用成员变量
2const 成员变量
3自定义类型的成员 (没有默认构造函数)
即:这些变量都是在定义时就要给初始值进行初始化的。
注意:
1构造函数中的语句只能将其赋值(可以多次), 不叫初始化(只能初始化一次,即每个成员对象只能在初始化列表中出现一次)
2初始化列表有值则用初始化列表的 不用声明时给缺省值的缺省值 而是用构造函数的缺省值
class A
{
};
class B
{
B(int i)
{
}
};
class Date1
{
public:
Date1(int year, int month, int day,int &x) ://这里更优先
_year(year)
, _month(month)
, _day(day)
,n(2)
,ref(x)
//,_b(1)
{
//赋值修改
_year = year;
_month = month;
_day = day;
}
private:
//声明时给缺省值
int _year=2024;
int _month=1;
int _day=1;
const int n=1;
int& ref;
A _a; //有默认构造函数 初始化时直接调用 不用初始化列表初始化
B _b; //无默认构造函数 且无初始化列表初始化 报错 “B”: 没有合适的默认构造函数可用
};
7.隐式类型转化 explicit
作用:explicit 修饰的构造函数有禁止隐式类型转换
单参数构造函数支持隐式类型转化
c++11支持多个没有默认值参数的构造函数的隐式类型转化: 用大括号
Date d1 = 2021;
Date tmp(2021); //先构造
Date d1(tmp); //再拷贝构造
//优化后
Date d1(2021);
对于第一行代码:以前的编译器会进行隐式类型转换,先构造一个临时对象再,再拷贝构造
现在编译器优化了,会直接执行第四行代码
class C
{
public:
C(int x, int y)
{
}
int x;
int y;
};
int main()
{
C c1(1, 2);
C c2 = { 1,2 };
return 0;
}
例子
class C
{
public:
//explicit C(int i)
C(int i)
:_c(i)
{
}
int _c;
};
class Stack {
public:
void Push(const C& s)
{
}
};
int main()
{
//用处
Stack s;
C cc1(3);
s.Push(cc1);//既可以传C类型的对象 也可以传单参数构造函数单参数
//或者时第一个参数无默认值的构造函数其他参数都有默认值
s.Push(3);//隐式类型转化成C类型
return 0;
}
C c4=5;
//编译错误C& c5 = 5;//引用的不是5而是5所构造的临时变量,用完即销毁 无法引用
正常写法
C c1(2);
C c2 = 1;
先构造一个临时对象,再拷贝构造
实际没有拷贝构造
编译器优化了,连续的构造会被合二为一
2.析构函数
1.作用
主要清理堆区的资源 防止内存泄漏
要显示写拷贝构造一般就要显示写析构函数,(因为涉及深浅拷贝问题)
2.特性
1无参无返回值 即无重载只有一个
2生命周期结束后自动调用
~A()
{
cout << "~A()"<<endl;
}
3.析构顺序
先实例的对象先构造后析构
为什么:因为对象是在函数中实例化的,函数调用会建立函数栈帧,对象的构造类似栈的先入后出。
构造顺序:全局对象(从上往下)-》局部静态-》(局部对象)
销毁循序:d3 d1 d0 d4 d2 d8 d7 d6 d5
局部对象(后定义先销毁)-》局部静态-》全局对象(后定义先销毁)
void func()
{
Date d3(3);
static Date d4(4);
}
Date d5(5);
static Date d6(6);
Date d7(7);
static Date d8(8);
int main()
{
Date d0(0);
Date d1(1);
static Date d2(2);
func();
}
3.拷贝构造
1.特性
对于未显式定义的拷贝构造
对内置类型按字节序完成拷贝 浅拷贝 (值拷贝)
对自定义类型,调用它的拷贝构造
写了拷贝构造没写构造函数,系统则不会生成默认构造函数,会编译错误,
Time() = default;//强制编译器生成
****拷贝构造也是构造 主动定义则编译器不会自动生成默认构造函数****
2.写法
注意:容易写出无穷递归
//拷贝构造
Date(Date d)//调用拷贝构造 要传值传参 又要构造拷贝构造 无穷递归
{
_day = d._day;
_month = d._month;
_year = d._year;
}
*****************************以上错误例子
Date(const Date& d)正确写法
{
_day = d._day;
_month = d._month;
_year = d._year;
}
4.运算符重载
作用:增强代码可读性
赋值运算符重载 d2=d1; 先有d2
区分拷贝构造 Date d2(d1);建立d2
1.特性
1赋值运算符不显式实现则系统自动生成。
2不能重载的符号 sizeof :: .* ?: . 这五个。
3重载操作符必须要有类类型或枚举类型。
3同拷贝构造 浅拷贝不需要 深拷贝要写赋值重载
2.注意
可以写在类外,此时外部无法访问类内的成员变量,可以将成员变量设置为(public)(极度不推荐)
可以用友元,由于类外函数是没有this指针,要用两个形参
Date& operator=(const Date& d)// 赋值运算符重载函数
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
注意要点
1.参数用const&减少拷贝构造
2.返回值用引用,为了支持连续赋值,返回this*
3.赋值前必须检查是否给自己赋值,首先为了减少没必要的拷贝,其次要是对于链表中的赋值重载,要先delete自身节点再赋值那就会有错
一段有意思代码
Date d1(2021, 6, 1);
Date d2(d1);
Date d3 = d1;
对于第三行代码,其实还是拷贝构造。
拷贝构造是用一个已有的对象去构造另一个还没有实例化的对象。
赋值运算符重载是两个对象已经存在的情况下,用一个对象赋值给另一个对象。
const
1.const修饰类的成员函数
void print()const
{
cout<<"haha":
}
const 修饰this指针指向的内容(this指针是形参),即表明这个函数中不能对this指针指向的对象进行修改。
实际上:由于函数是自己设计的,肯定知道是否要修改,
所以作用也有:让const成员对象也能调用。
一般用法:
成员函数只读 用const
成员函数既读又写 不用const
1.const对象可以调用非const成员函数吗?
不可以:权限放大
2.非const对象可以调用const成员函数吗?
可以
3.const成员函数内可以调用其他的非const成员函数吗?
不可以,同样权限放大
4.非cosnt成员函数内可以调用其他的cosnt成员函数吗?
可以
注意在类中的成员函数,const修饰的形参有以下作用
1.const,让非const成员变量调用 是this指针指向的对象不可改
2.为了符合隐式类型转换时满足,临时变量具有常性,使赋值顺利
2.权限的放大与缩小
传参 引用传参 const 类型&
int a=0;
int &b1=a;
const int &b2=a//权限的缩小 正确
const int c=1;
int &d=c;//权限的放大 错误
int a = 0;
int b = 1;
const int& c = 19;//可以 10为常量 即可读不可改
const int& d = a + b;//a+b计算的表达式的结果为临时变量 具有常性 可以
int& e = a + b;//错误
double d =1.1;
int i=d; //隐式类型转化 先拷贝到临时变量 具有常性 即可读不可改
const int& ri=d; //隐式类型转化 可以
int &ri =d; 不可以隐式类型转化
5.取地址及const取地址操作符重载
一般不用重新定义,使用编译器自动生成的就行
static
用处:如统计构造函数调用了多少次
想定义全局变量 又想用类封装 用静态成员变量
不想这个变量被访问 放在私有,用静态static成员函数访问
1.静态成员变量
static int n; //n不属于某一个对象,属于整个类
特性
1因此static 修饰的变量不能在初始化列表里初始化
2只能在类内声明,类外定义 定义才给初始值
定义 定义时不用static关键字 有类域
3若n为私有成员 则要用静态成员函数调用
class A
{
public:
static int GetN()
{
return n;
}
private:
static int n;
}
int A::n = 0;
访问静态成员变量的方法
1.静态成员变量要是公有
1.通过类对象突破类域进行访问
3.通过匿名对象突破类域进行访问
2.通过类域突破类域进行访问(一般比较常用)
cout << a._n << endl; //1.通过类对象突破类域进行访问
cout << A()._n << endl; //3.通过匿名对象突破类域进行访问
cout << A::_n << endl; //2.通过类名突破类域进行访问
2.当静态成员变量为私有时
1.通过类对象调用静态成员函数进行访问
2.通过匿名对象调用静态成员函数进行访问
3.通过类域调用静态成员函数进行访问
cout << a.GetN() << endl; //1.通过对象调用成员函数进行访问
cout << A().GetN() << endl; //2.通过匿名对象调用成员函数进行访问
cout << A::GetN() << endl; //3.通过类名调用静态成员函数进行访问
2.静态成员函数
作用1:访问静态成员变量
要点:
1.静态成员函数无法访问对象的非静态变量,因为没有this指针
2.非静态成员函数可以调用静态成员函数,因为在同一个类域
class A
{
public:
A()
{
n++;
}
A(const A& a)
{
n++;
}
//static成员//
static int GetN()
{
return n;
}
private:
static int n;//n不属于某一个对象,属于整个类
//因此static 修饰的变量不能在初始化列表里初始化
//只能在类内声明,类外初始化
//若n为私有成员 则用静态成员函数调用
};
int A::n = 0;
A Func()
{
A aa3;
return aa3;//拷贝构造
}
int main()
{
A aa1;
A aa2;
Func();
//cout << aa1.n<<endl<<aa2.n;//4次 n为公有成员
cout << A::GetN() << endl; //N为私有成员
return 0;
}
friend友元
1.友元函数
1友元函数不是成员函数,即不能用const修饰 无this指针
2友元声明 流插入流提取不能写在类内//cout cin只能做全局函数
friend istream& operator>>(istream& in, Date& d);
friend ostream& operator<<(ostream& out, const Date& d);
3友元函数可以在类定义的任何地方声明,不受限定符限制
2.友元类
声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
friend class Date;
内部类
不属于外部类
1.外部类天生就是内部类的友元
2.外部类可以访问内部类 反过来不行
3.sizeof(外部类) = 外部类,和内部类没有任何关系
匿名对象
非匿名对象
A aa1;
A aa2(10);
匿名对象生命周期和作用域只在当前一行
A();
使用场景 只为调用一个函数 或传个参
如vector<T>中的resize模拟实现
void resize(size_t n,const T& val=T());