类
相对于c语言的结构体,在c++中更喜欢用类这种类型,当然也兼容结构体
在类中可以定义成员变量,成员函数,成员函数可以访问成员变量
class stack
//stack是类名
{
//成员函数
void Init()
{
a=nullptr;
top=capacity=0;
}
//成员变量
int* a;
int top;
int capacity;
};
int main()
{
stack S2;
return 0;
}
类的定义
class是定义类的关键字,{}里面是类的主体,和结构体一样{}后要加 ;
class与struct的默认权限不一样
class默认限定符是私有的
strucr的默认限定符是公有的
访问限定符
public(公有)
protection(保护)
private(私有)
类定义了新的作用域,类的所有成员都在域中
pubilc修饰的成员可以在类外面访问
protection和private修饰的成员只能在类里面访问,不能直接在类的外面被访问
在类中以上两个私有关键字不受限定符限制
class stack
{
pubilc:
void push(int x)
{
if(capacity==top)
{
int newcapacity=capacity==
0?4:capacity*2;
a=(int*)realloc(a,sizeof(int)*newcapacity);
capacity=newcapacity;
}
a[top++]=x;
}
void Init()
{
int*a=nullptr;
top=capacity=0;
}
private:
//修饰从24行开始直到遇到下一个限定符结束
//private修饰以后外部无法访问
int* a;
int top;
int capacity;
};
int main()
{
return 0;
}
类可以成员和定义分离
如果成员函数在类里面定义,编译器可能会把它当做内联函数处理
当类在外定义成员时,需要使用::作用域操作符指明成员属于哪个类的域
//.h文件声明
class stack
{
void Init();
void push(int x);
private:
int* a;
int top;
int capacity;
};
//.cpp文件定义
//stack::说明它是成员函数,且可以在这个类中找其他成员变量
//不加的话会找不到成员,因为不在这个域中
void stack::Init()
{
int*a=nullptr;
top=capacity=0;
}
void stack::push(int x)
{
if(capacity==top)
{
int newcapacity=capacity==
0?4:capacity*2;
a=(int*)realloc(a,sizeof(int)*newcapacity);
capacity=newcapacity;
}
a[top++]=x;
}
用_来区分成员变量,或是前置,或是后置,或是 _m,是一种书写风格
类的实例化
类的定义不需要分配内存空间,而去使用类创建一个对象,才会占用空间
成员函数是存储在公共代码区的,所以sizeof计算类时不会计算成员函数的大小
class date
{
pubilc:
void Init (int year,int month,int day)
{
_year=year;
_month=month;
_day=day;
}
private:
//这里的变量都是声明,声明没有开空间,没有定义是没有空间的
int _year;
int _month;
int _day;
};
int main()
{
date._year=1;
//这里是不可以的因为上面的类只是声明
//开空间
date d;
d._year=1;
//这里还是不行赋值,因为是私有的
}
#include<iostream>
using namespace std;
class a1
{ public:
void f1(){}
private:
int _a;
};
class a2
{
public:
void f2(){}
};
class a3
{};
int main()
{
cout<<sizeof(a1)<<endl;
cout<<sizeof(a2)<<end2;
cout<<sizeof(a3)<<end3;
return 0;
}
//结果 4,1,1
为什么一个没有成员变量名,一个没有成员,大小还能为1呢?
因为这两个类进行了占位,c++对没有成员变量的类分配1byte的空间,进行占位表示对象存在
为什么要内存对齐?
cpu访问内存时并不是从任意位置随意访问,只能是从内存地址开头位置去访问
对齐下的地址,每个值之间都会有没用的指针给隔开
不对齐的地址,每个值的地址都是相邻的
在不对齐的情况下,cpu从 _a开始向下访问,才能访问到 _b,期间读取 _a时要读前三个字节的数据,在读 _b的前一个字节,再进行拼接才能读出 _a的值,期间要涉及数据拼接,多次读取,还可能读取到不需要的数据
而在对齐情况下 从上向下访问到 _b只需要一次,从而提高了效率
this指针
隐藏的this指针,this指向当前调用这个函数的对象,在c++中是个关键字
只对非静态的成员函数有效
只能在成员函数内部使用
//编译器自动添加this
class Date
{
pubulic:
//this在实参和形参位置不能显示的写
//但是在类里面可以显示使用
void Init(/*Date* this*/,int year,int month,int day)
{
/*this->*/_year=year;
/*this->*/_month=month;
/*this->*/_day=day;
}
private:
int _year;
int _month;
int _day;
};
//调用的地方
int main()
{
Date d;
d.Init(/*&d*/,2023,7,27);
return 0;
}
通过this访问的对象,this是个成员函数形参,一般存储在栈帧中,所以对象不存储编译器一般会用寄存器自动传递
class p
{
public:
void print()
{
cout<<"print"<<emdl;
}
private:
int _a;
};
int main()
{
p* d=nullptr;
d->print();
//这段代码没有解引用,因为print函数存在在公共代码区,不需要在类中寻找
(*d).print();
//这段看上去解引用了,其实并没有,因为编译器不需要找print函数,所以和d->print()一样
d->_a;
//当访问类的成员时就会崩溃,因为d是空指针
return 0;
}
默认成员函数
当一个类为空,并非什么都没有,编译器会自动生成六个默认成员函数
分别是负责初始化与清理的,构造函数和析构函数
负责拷贝赋值的,拷贝构造函数和赋值重载函数
负责取地址重载的,取地址重载函数
构造函数
构造函数是个特殊的成员函数,构造函数的函数名和类名相同
构造函数的作用是初始化对象,而不是开辟空间
特征
在对象的生命周期内只调用一次
没有返回值
对象实例化时自动调用
构造函数可以进行重载
class Date
{
public:
//无参构造
Date()
{
_year=1;
_month=1;
_day=1;
}
//带参数构造
Date(int year,int month,int day)
{
_year=year;
_month=month;
_day=day;
}
//全缺省版
Date(int year=1,int month=1,int day=1)
{
_year=year;
_month=month;
_day=day;
}
//从语法上看这几个函数构成函数重载可以共存,但是调用会出现问题,因为编译器不知道要调用哪个
void print()
{
cout << _year << endl;
cout << _month << endl;
cout << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//无参初始化
//使用无参构造函数时,对象后不加括号,否则就是函数声明
Date a;
a.print();
//带参初始化
Date b(2023,7,27);
b.print();
return 0;
}
//执行结果 1,1,1
默认构造函数
一般情况下都需要手动写构造函数,决定初始化方式,手动编写后,编译器就不会自动生成
只有在成员变量全是自定义类型,才考虑不写构造函数
内置类型(语言提供的类型,只要是指针都是内置类型),对于内置类型成员编译器不会处理(并不绝对)
无参的构造函数,全缺省的构造函数,编译器生成的构造函数,统称默认构造函数,这三个只能有一个,否者会出现调用歧义
初始化列表
以冒号开始接着以逗号分割数据的成员列表,每个成员变量后跟着一个放在括号里的初始值或是表达式
尽量使用初始化列表初始化,不论是否使用初始化列表,对于自定义变量,一定会优先使用初始化列表进行初始化
每个成员变量只能初始化一次
引用成员变量
const修饰成员变量
自定义类型成员且无默认构造函数
这三种必须在初始化列表进行初始化
成员变量在类中的声明顺序决定了其在初始化列表的初始化顺序
class D
{
public:
D(int &a)
:_a(a)
{}
private:
int _a;
}
class Date
{
public:
Date(int year,int month,int day,int &n)
{
//初始化列表是成员定义的地方
:_year(year)
,_month(month)
,_n(0)
,_end(n)
,_D(1)
//,_day(day)
//对于day在初始列表,也是定义了的但是因为没有编写,内置类型编译器不做处理,所以给了随机值
//无论是有没有显示定义,初始化列表都会去定义,自定义类型或调用默认构造,内置类型给随机值,部分编译器会给零,显示定义的构造函数的自定义类型,初始化列表不作处理
//也可以混合使用
{
//赋值
_day=day;
}
}
private:
//成员声明
int _year=1;
//c++11支持给缺省值,这个缺省值是给初始化列表的
//如果初始化列表没有显示定义,就使用这个缺省值
//若是初始化列表定义了,缺省值无效(缺省值主要是备用)
int _month;
int _day;
//const必须在定义时初始化
const int _n;
//引用必须在定义时初始化
int &_end;
//自定义类型没有构造需要在定义时初始化
D _D;
};
calss a
{
public:
A(int i)
:_n(i)
{}
A(const a&i)
:_n(i._n)
{}
private:
int _n;
};
int main()
{
//初始化
a A(1);
//单参数构造函数隐式类型转换
//2会调用a构造函数生成一个临时对象,再用这个临时对象拷贝构造A1
//编译器优化后,直接将2构造
a A1=2;
return 0;
}
析构函数
析构函数负责对象在销毁时完成对象的资源清理工作
析构函数是在类名前加上 ‘~’
无参无返回值,不能重载
一个类只能有一个析构函数,没有手动编写,编译器会自动生成默认析构函数(对于内置类型不做处理)
对象生命周期结束时,编译器自动调用析构函数
后定义的先析构,类似栈帧的后进先出
class Date
{
~Date()
{
cout << _year << endl;
}
void print()
{
cout << _year << endl;
cout << _month << endl;
cout << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date a(2000);
// a.print();
Date b(2023);
// b.print();
return 0;
}
//执行结果2023,2000
类中无资源申请可以不去编写析构函数,但是一旦有资源申请就需要去编写,防止资源泄露
拷贝构造
拷贝构造函数是构造函数的重载
拷贝构造只有一个形参,是对类类型对象的引用,在不涉及改变源数据的情况下,经常加const来修饰
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//错误
//Date(const Date d)
//正确
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d;
Date D(d);
return 0;
}
默认拷贝构造是按对象内存储字节序完成拷贝的,这种拷贝又叫浅拷贝
无穷递归
如果传值方式并非引用传参,在调用函数之前会调用拷贝构造函数,而调用拷贝函数又需要传参,又会调用拷贝函数循环往复,就成了死循环
所以要一定使用引用传参
class Stack
{
public:
Stack(size_t capacity=0)
{
_a = (int*)malloc(sizeof(int) * capacity);
if (_a == NULL)
{
perror("malloc fail");
return;
}
_top = 0;
_capacity = capacity;
}
void Push(int val)
{
if (_top == _capacity)
{
_a = (int*)realloc(_a, sizeof(int) * (_capacity + 4));
if (_a == NULL)
{
perror("realloc fail");
return;
}
}
_a[_top] = val;
_top++;
}
~Stack()
{
free(_a);
_top = _capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack S;
Stack s1(s);
return 0;
}
像栈之类的类在使用浅拷贝时会有多次析构的问题,会造成程序崩溃
自定义类型传值传参必须调用拷贝构造
自定义类型需要自定义构造拷贝函数
赋值是两个已经存在的对象进行拷贝,拷贝构造,是一个存在的对象,去创建另一个新对象
拷贝优化
拷贝对象优化
一个表达式,连续的构造(包括拷贝构造)可能会被合并
A f2()
{
A aa;
return aa;
//优化直接构造
return A(1);
}
int main()
{
//拷贝
A ret1=f2();
//这是一个表达式,当aa返回临时对象之前编译器将它直接拷贝给了ret1
A ret2;
//赋值
ret2=f2();
//此时便不会优化了
//优化条件需要二者都是构造,得是一个表达式
return 0;
}
//看似两次拷贝构造,但是被编译器给优化了
赋值运算符重载
自定义类型不允许使用运算符直接进行比较
关键字operator后接需要重载的运算符符号
如bool operator<(参数)
不能创建新的操作符
重载操作符必须有一个类类型参数
运算符重载必须有一个参数是自定义类型
内置类型运算符,的含义不能改变,如‘+’运算符不能改成其他意思
不能改变操作符的操作个数
如,+ 有着两个操作数,但是因为有着隐藏的this指针存在,所以只需要重载函数只需要一个参数
- ::
- sizeof
- ?:
- .*
- .
这几个是不能重载的
class Date
{
public:
Date(int year=1,int month=1,int day=1)
{
_year=year;
_month=month;
_day=day;
}
private:
int _year;
int _month;
int _day;
}
//比较函数
//bool Compare(const Date&c1,const Date&c2)
//运算符重载
//将运算符重定义
bool operator<(const Date&c1,const Date&c2)
{
if(c1._year<c2._year)
{
return true;
}
else if(c1.year==c2.year&&c1.month<c2.month)
{
return true;
}
else if(c1.year==c2.year&&c1.month==c2.month&&c1.day<.c2.day)
{
true;
}
else
return false;
}
int main()
{
Date d1(2023,1,1);
Date d2(2023,2,2);
//不加括号的话会使cout<<d1先结合
cout<<(d1<d2)<<endl;
return 0;
}
class Date
{
public:
Date(int year=1,int month=1,int day=1)
{
_year=year;
_month=month;
_day=day;
}
//其中this参数指向d1
bool operator<(const Date&c2)
{
if(_year<c2._year)
{
return true;
}
else if(_year==c2._year&&_month<c2._month)
{
return true;
}
else if(_year==c2._year&&_month==c2._month&&_day<c2._day)
{
true;
}
else
return false;
}
private:
int _year;
int _month;
int _day;
}
int main()
{
Date d1(2023,1,1);
Date d2(2023,2,2);
cout<<d1.operator(d2)<<endl;
return 0;
}
//赋值运算符重载
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
注意赋值运算符重载
返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
检测是否自己给自己赋值
返回this指针要符合连续赋值的含义
赋值运算符只能重载成类的成员,不能重载为全局函数
class Date
{
public:
//获取某年某月的天数
int GetMonthDay(int year, int month)
{
static int a[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31};
if (month == 2 && year % 4 == 0 && year % 100 != 0 || year % 400 == 0)
{
return 29;
}
return a[month];
}
//默认构造
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//赋值运算符重载
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
//析构函数
~Date()
{
_year = 0;
_month = 0;
_day = 0;
}
//日期+=天数
Date& operator+=(int day)
{
_day += day;
while (_day > this->GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month == 13)
{
_year++;
_month = 1;
}
}
return *this;
}
//日期+天数
Date operator+(int day)
{
Date tmp(*this);
tmp+= day;
return tmp;
}
void print()
{
cout << _year <<"年" << _month << "月" << _day << "日"<<endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023,7,31);
Date d2=d1;
//d1给d2是拷贝构造
Date d3;
d3=d1;
//d1给d3是赋值重载
return 0;
}
拷贝构造是使用同类对象初始化创建对象
赋值重载主要是把一个对象赋值给另一个对象
对于自定义类型,默认赋值重载,是以字节拷贝的方式赋值的,需要同样小心,多次析构的问题
前置++和后置++
//前置++
Date& operator++()
{
*this += 1;
return *this;
}
//后置++
Date operator++(int)
{
Date tmp(*this);
*this +=1;
return tmp;
}
默认前置++不需要参数,而后置++需要一个int类型来进行占位,以便区分二者
前置–和后置–同理
const 成员函数
使用const修饰的成员函数,叫const成员函数,实际上修饰的是this指针,表示不能对类的任何成员进行修改
class Date
{
public:
void print()const;
//等同
void print(const Date*this);
const int func();//修饰返回值无法修改返回值
//效果等同于int func();返回一个拷贝值,这个值具有常性无法修改
private:
int _year;
int _month;
int _day;
}
由于隐藏的this指针无法显示定义存在实参和形参所以只能将const放在表达式后方来修饰this
const对象无法调用普通成员函数,这是权限的放大
void Display()const==void Display(const Date*this)
void print()const;
void print();
//这两个函数可以同时存在,参数不同可以重载
缺省参数可以调用函数
class SeqList
{
pubilc:
int ope
private:
int* _a=(int*)malloc(sizeof(int)*10);
size _top=0;
size _capacity=0;
};
不能无脑加const
只读内部不需要修改的成员都可以加const
const对象可以调用非const成员函数吗?不能这是权限的放大
非const对象可以调用const成员函数吗?可以这是权限的缩小
const成员函数内可以调用其他非const成员函数吗?不可以这是权限的放大
非const成员函数内可以调用其它const成员函数吗?可以这是权限的缩小
取地址和const运算符重载
取地址运算符是默认成员函数,编译器自动生成
除非在特殊情况下不想被取出地址,需要自己编写,其他情况无需编写
class Date
{
public:
Date* operator&()
{
return this
}
const Date* operator&()const
{
return this ;
}
private:
int _yaer;
int _month;
int _day;
};
隐式类型转换
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值 的构造函数,还具有类型转换的作用
c++98支持单参数隐式类型转换
c++11支持多参数隐式类型转换
#include<iostream>
class A
{
public:
//explicit A(int i)
//修饰构造函数,禁止类型转换
A(int i)
:_a(i)
{}
A(const A&a)
:_a(a._a)
{}
private:
int _a;
};
class B
{
public:
B(int b1,int b2)
:_b(b1)
,_bb(b2)
{}
private:
int _b;
int _bb;
};
int main()
{
A a1=1;
//单参数构造函数的隐式类型转换
//1调用一个构造函数生成一个临时对象,再用临时对象调用拷贝构造到a1
//这个过程编译器会优化成用1直接构造
A1&a = 1;
//这是错误的因为类型转换都会生成临时变量,它具有常性,这是权限的放大
//const A1&a=1;权限的平移
//c++11 支持多参数隐式转换
B b1(1,2);
B b2={3,4};
return 0;
}
关键字explicit修饰构造函数,禁止类型转换
友元
友元破坏了类的封装性,不适合多用
可以直接访问私有的成员,它是定义在类外部的普通函数,不属于类,但是需要在类的内部声明,声明要加friend关键字
友元函数
友元函数可以访问类的私有和保护成员,但它本身不是类的成员函数
友元函数不能用const来修饰
友元函数可以在类的任何地方声明,不受访问限定符的限制
一个函数可以是多个类的友元函数
友元函数的调用与普通函数调用相同
class Date
{
public:
//友元声明,声明只要放在类中,在哪都一样
friend ostream& operator<<(ostream& _cout, const Date& d);
friend istream& operator>>(istream& _cin, Date& d);
private:
int _year;
int _month;
int _day;
//流插入流提取,不能定义成成员函数,因为有隐藏指针this,会导致符号指向错误
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()
{
return 0;
}
友元类
友元关系是单向的
如,B类可以访问A类的成员,但是A类不能访问B类成员
友元关系不能传递
如,C是B的友元, B是A的友元,不能说明C是A的友元
友元关系不能继承
class A
{
friend class B;
//声明B类为A的友元类,在B类中就可以访问A类中的私有成员变量
public:
A(int a = 1, int b = 2, int c = 3)
: _a(a)
, _b(b)
, _c(c)
{}
private:
int _a;
int _b;
int _c;
};
class B
{
public:
B(int aa=0,int bb=0,int cc=0)
: _aa(aa)
, _bb(bb)
, _cc(cc)
{}
void print()
{
cout << t._a << ' ' << t._b << ' ' << t._c << endl;
}
private:
int _aa;
int _bb;
int _cc;
A t;
};
int main()
{
B bb;
bb.print();
return 0;
}
内部类
一个类定义在另一个类的内部,这就叫内部类
内部类受外部类类域和访问限定符的限制
内部类是外部类的友元
内部类可以访问外部类,而外部类不能访问内部类
//这两个类各自独立
class A
{
public:
class B
{
public:
void func()
{
A aa;
_b = aa._a;
}
private:
int _b;
};
private:
int _a = 1;
};
匿名对象
//有名对象,生命周期在当前局部域
A aa(1);
//匿名对象,生命周期在这一行
A(7)
静态成员
int n=0,m=0;
class A
{
public:
A()
{
++n;
++m;
}
A(const A& t)
{
++n;
++m;
}
~A()
{
--m;
}
//静态成员函数无this指针
static void print()
{
cout<<n<<' '<<m<<endl;
}
private:
//放入类中
//加上static使变量成为全局的属于所有对象
//但是缺省值是在初始化列表使用的,而这种静态变量无法在初始化列表初始化
//static int n=0;
//static int m=0;
//声明
static int n;
static int m;
};
//定义
int A::n=0;
int A::m=0;
int main()
{
A a1;
A a2;
//如果类成员变量n,m是公共的,以下一切建立在公有的基础上
//有这几种访问方式
A::n;
//通过A访问类域中的n
a1.n;
A* ptr=nullptr;
ptr->n;
//ptr->n
//上面两种方式是通过它们访问类域,从而找到n,m
//而n,m是存在在静态区的
//不用创建对象直接使用静态成员函数
A::print();
}
全局变量会破坏封装,把它们放入类中,受到类的限制,但是这样会导致每个对象都有n,m
在n,m前加上static修饰成静态成员变量
静态成员函数没有this指针,所以不能用const来修饰
静态成员函数不能访问非静态变量,因为没有this指针