1.面向对象和面向过程的初步分析
C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
就例如洗衣服的这个行为,C语言中通过过程来描述事情,而C++则是面向对象,关注点是在对象的身上,将事件拆分为不同的对象,从而进行编程。
2.类的定义
class ClassName
{
//...
//在花括号中间定义变量和函数
}; //要加分号
类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数。
2.1 类的定义方法
1.声明和定义都放在类体中,成员函数在类中定义可能会被编译器当作内联函数展开。
Class Date
{
public:
void Print()//成员函数
{
cout<<"Print()"<<endl;
}
//这里就是将成员函数声明和定义放在类里面
private:
//成员变量
int year;
int month;
int day;
};
2.类的声明放在头文件内,成员函数的定义放在.cpp文件内,这种方式使用成员函数需要加类名。
//.h的头文件内
Class Date
{
//函数的声明
public:
void Print();
private:
int year;
int month;
int day;
};
//.cpp的文件内
#include".h"
void Date::Print()
{
//函数的实现
}
以上这两种方式,第一种比较适合在练习中使用,第二种推荐在工作或者项目中使用。
3.类的访问限定符和封装
3.1 访问限定符
C++中一共有三种访问限定符,分别为public、private、protected。public修饰的成员可以直接在类外面进行访问和修改,private和protected两者并不能。
【访问限定符说明】
1.访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止。
2.如果后面没有访问限定符,作用域就到} 即类结束。
3.class的默认访问权限为private,struct为public(因为struct要兼容C)。
3.2 封装
面向对象的三大特性:封装、继承、多态。
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。
class Person
{
public:
void PrintPersonInfo();
private:
char _name[20];
char _gender[3];
int _age;
};
int main()
{
Person p1;
//p1._name = "zs";
//语法错误,Person类里的成员变量被private限定了,不能直接访问
return 0;
}
4. 类的实例化
1. 类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它;比如:入学时填写的学生信息表,表格就可以看成是一个类,来描述具体学生信息。
class Person
{
public:
void PrintPersonInfo();
private:
//这里的成员变量都只进行了声明,未定义,因此并没有开空间。
char _name[20];
char _gender[3];
int _age;
};
2. 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量。
int main()
{
Person._name = "张三";
//编译失败:error C2059: 语法错误:“.”
return 0;
}
3. 做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间。
int main()
{
Person p1;//实例化对象
p1.PrintPersonInfo();//调用成员函数
return 0;
}
5. 类的对象模型
5.1 类的大小
class A
{
public:
void PrintA()
{
cout<<_a<<endl;
}
private:
char _a;
};
我们都知道,这个类里面包含着一个成员函数和一个成员变量,那我们如何来计算这个类的大小呢???
如果将每个对象都包含类的各个成员,每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份代码,相同代码保存多次,浪费空间。
int main()
{
Person p1;
Person p2;
Person p3;
Person p4;
Person p5;
Person p6;
Person p7;
//如果是每个实例化对象都有类的成员,代码太多,浪费空间,
return 0;
}
综上述,C++采用的方法是将类里面的成员函数统一的放到公共代码段里面,类里面只保存成员变量。
5.2 结构体内存对齐规则
1. 第一个成员在与结构体偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的对齐数为8
3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
6. this指针
6.1 this指针的引出
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout <<_year<< "-" <<_month << "-"<< _day <<endl;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
int main()
{
Date d1, d2;
d1.Init(2022,1,11);
d2.Init(2022, 1, 12);
d1.Print();
d2.Print();
return 0;
return 0;
}
以上这个代码,Date类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调用 Init 函数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?
C++中通过引入this指针解决该问题,即:C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
class Date
{
public:
//void Init(Date* const this,int year,int month,int day);
void Init(int year, int month, int day)
{
_year = year;//this->_year = year;
_month = month;//this->_month = month;
_day = day;//this->_day = day;
}
//void Print(Date* const this)
//this就是隐含的指针,不能给this赋值,为形参。
void Print()
{
cout <<_year<< "-" <<_month << "-"<< _day <<endl;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
7. 类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。但是编译器生成的默认成员函数大部分情况下不能满足我们的需求,需要我们自己来实现。
7.1 构造函数
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象(我们常使用的局部对象是栈帧创建时,空间就开好了),而是对象实例化时初始化对象。构造函数的本质是要替代我们以前Stack和Date类中写的Init函数的功能,构造函数自动调用的特点就完美的替代的了Init。
构造函数的特点:
1.函数名与类名相同
2.无返回值(连void都不用写)
class Date
{
Date(){}
//最简单的构造函数
};
3.构造函数可以重载
4.如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
class Date
{
public:
//无参构造函数
Date(){}
//传参构造函数
Date(int year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
//全缺省构造函数,与无参的只能存在一个
Date(int year = 1, int month = 1, int day = 1)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;//调用默认构造函数
Date d2(2024,07,10);//调用传参构造函数
//Date d3(); 这样与函数的声明区分不开,产生歧义
return 0;
}
5.无参构造函数、全缺省构造函数、我们不写构造时编译器默认生成的构造函数,都叫做默认构造函数。但是这三个函数有且只有一个存在,不能同时存在。无参构造函数和全缺省构造函数虽然构成函数重载,但是调用时会存在歧义。
6.编译器默认生成的构造,对内置类型成员变量的初始化没有要求,。对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要用初始化列表才能解决。
typedef int STDataType;
class Stack
{
public:
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_top = 0;
}
// ...
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
// 两个Stack实现队列
class MyQueue
{
public:
//编译器默认生成MyQueue的构造函数调用了Stack的构造,完成了两个成员的初始化
private:
Stack pushst;
Stack popst;
};
7.1.1 再探构造函数
之前我们实现构造函数时,初始化成员变量主要使用函数体内赋值,构造函数初始化还有一种方式,就是初始化列表,初始化列表的使用方式是以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
每个成员变量在初始化列表中只能出现一次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地方。
引用成员变量,const成员变量,没有默认构造的类类型变量,必须放在初始化列表位置进行初始化,否则会编译报错。
class A
{
public:
A(int a)
:_a1(a){}
private:
int _a1;
};
class B
{
public:
B(int& b2)
:_b1(b1)
,_b2(b2)
,_a3(a3)
,_b4()
{
// error C2512: “A”: 没有合适的默认构造函数可用
// error C2530 : “B::_b2” : 必须初始化引用
// error C2789 : “B::_b1” : 必须初始化常量限定类型的对象
}
private:
const int _b1;//const 常量
int& _b2;//引用
A _a3;//没默认构造
// 注意这里不是初始化,这里给的是缺省值,这个缺省值是给初始化列表的
// 如果初始化列表没有显示初始化,默认就会用这个缺省值初始化
int _b4 = 1;
};
C++11支持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显示在初始化列表初始化的成员使用的。
在初始化列表初始化的成员也会走初始化列表,如果这个成员在声明位置给了缺省值,初始化列表会用这个缺省值初始化。如果你没有给缺省值,对于没有显示在初始化列表初始化的内置类型成员是否初始化取决于编译器,C++并没有规定。对于没有显示在初始化列表初始化的自定义类型成员会调用这个成员类型的默认构造函数,如果没有默认构造会编译错误。
初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的的先后顺序无关。建议声明顺序和初始化列表顺序保持一致。
7.2 析构函数
析构函数与构造函数功能相反,析构函数不是完成对对象本身的销毁,比如局部对象是存在栈帧的,函数结束栈帧销毁,他就释放了,不需要我们管,C++规定对象在销毁时会自动调用析构函数,完成对象中资源的清理释放工作。析构函数的功能类比我们之前Stack实现的Destroy功能,而像Date没有Destroy,其实就是没有资源需要释放,所以严格说Date是不需要析构函数的。
析构函数的特点:
1.函数名是在类名前面加~。
2.无返回值(连void都不用写)。
3.一个类有且只有一个析构函数,若未定义,系统自动生成默认的析构函数。
4.对象生命周期结束时,自动调用析构函数。
typedef int STDataType;
class Stack
{
public:
Stack(int n = 4)//构造函数
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_top = 0;
}
~Stack()//析构函数
{
free(_a);
_a = nullptr;
_capacity = _top =0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
int main()
{
Stack st1;//当对象生命周期结束时,自动调用析构函数
Stack st2;
//多个对象都要析构的情况下,后定义的先析构,这里就是先析构st2,再析构st1
return 0;
}
5.跟构造函数类似,我们不写编译器自动生成的析构函数对内置类型成员不做处理,自定类型成员会调用他的析构函数。
typedef int STDataType;
class Stack
{
public:
Stack(int n = 4)
{
//偷个懒qwq
}
~Stack()//析构函数
{
free(_a);
_a = nullptr;
_capacity = _top =0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
// 两个Stack实现队列
class MyQueue
{
public:
//编译器默认生成MyQueue的构造函数调用了Stack的构造,完成了两个成员的初始化
private:
Stack pushst;
Stack popst;
};
int main()
{
MyQueue mq;//会自动调用两个Stack析构
return 0;
}
6.还需要注意的是我们显示写析构函数,对于自定义类型成员也会调用他的析构,也就是说自定义类型成员无论什么情况都会自动调用析构函数。
// 两个Stack实现队列
class MyQueue
{
public:
~MyQueue(){}//啥也没干的析构函数
private:
Stack pushst;//就当我写了Stack的析构吧
Stack popst;
};
int main()
{
MyQueue mq;
//这里即使我写的析构函数没内容,但是他依然会去调用Stack的析构
return 0;
}
7.3 拷贝构造函数
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数,也就是说拷贝构造是一个特殊的构造函数。
// 两个Stack实现队列
class MyQueue
{
public:
MyQueue(const MyQueue& mq)//最基础的拷贝构造函数
{
//就是特殊的构造函数,所以有着构造函数的诸多特点
//...
}
private:
Stack pushst;//就当我写了Stack的拷贝构造,也是懒了qaq
Stack popst;
};
int main()
{
MyQueue mq;
//拷贝构造函数调用的两种方法
mq1 = mq;
mq2(mq);
return 0;
}
拷贝构造的特点:
1. 拷贝构造函数是构造函数的一个重载。
2. 拷贝构造函数的参数第一个必须是类类型对象的引用,使用传值方式编译器直接报错,因为语法逻辑上会引发无穷递归调用。这句话什么意思呢,接下来我会用两段代码来为大家解释。
class Date
{
public:
Date(//...){//...}//默认构造函数,假设我们写了
//C++规定,传值传参要调用拷贝构造
Date(const Date d)//传值传参
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2 = d1;//拷贝构造的调用
return 0;
}
我们来看看这段代码的执行逻辑。首先我们传值进去,d是d1的拷贝,我们要去调用d的拷贝构造,然后再传值,再去调用,再传,然后一直循环下去,形成了无穷递归。
综上所述,我们需要写一个正确的拷贝构造函数,需要传引用传参,如果我们不想被传的对象内容被改变的话,我们则需要加上const进行限制。
class Date
{
public:
Date(//...){//...}//默认构造函数,假设我们写了
//C++规定,传值传参要调用拷贝构造
Date(const Date& d)//传引用传参,这里就是正确的写法了。
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
3. C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。
4. 若未显式定义拷贝构造,编译器会生成自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造。
5. 像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的拷贝构造就可以完成需要的拷贝,所以不需要我们显示实现拷贝构造。像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器自动生成的拷贝构造完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。像MyQueue这样的类型内部主要是自定义类型Stack成员,编译器自动生成的拷贝构造会调用Stack的拷贝构造,也不需要我们显示实现MyQueue的拷贝构造。这里还有一个小技巧,如果一个类显示实现了析构并释放资源,那么他就需要显示写拷贝构造,否则就不需要。
/!---下面这段代码可以看出系统自动生成的拷贝构造函数的缺陷。---!/
这一段代码是我没写拷贝构造函数,由系统自动生成的拷贝构造函数,由于是值拷贝,我们可以看到,st和st1的地址都是相同,这样的坏处就是,我们如果想要修改st1的top值,我们会将st的top值一并修改了,两个指针指向的都是同一块空间,这是不被允许的,而且当程序结束时,会调用析构函数,因为st1后定义先析构,对这块空间析构完后,st再析构,这块空间被析构两次,程序会崩溃。
6. 传值返回会产生一个临时对象调用拷贝构造,传值引用返回,返回的是返回对象的别名(引用),没有产生拷贝。但是如果返回对象是一个当前函数局部域的局部对象,函数结束就销毁了,那么使用引用返回是有问题的,这时的引用相当于一个野引用,类似一个野指针一样。传引用返回可以减少拷贝,但是一定要确保返回对象,在当前函数结束后还在,才能用引用返回。
Stack func1()//传值返回
{
Stack st;
return st;
}
Stack& func2()//传引用返回
{
Stack st;
return st;
}
int main()
{
//由于调用函数时候会建立栈帧,st会被保存起来,会调用一次拷贝构造,
//再将保存起来的ecx,再次拷贝一次放到ret中,总共两次拷贝。
Stack ret1 = func1();
//但传引用可以减少拷贝,传引用返回的时st的别名,但是st出了函数作用域就销毁了
//return的引用就会变成野引用,需要在保证对象出了作用域还存在才可以使用。
Stack ret2 = func2();
return 0;
}
8. 赋值运算符重载
8.1 运算符重载
1.当运算符被用于类类型的对象时,C++语言允许我们通过运算符重载的形式指定新的含义。C++规定类类型对象使用运算符时,必须转换成调用对应运算符重载,若没有对应的运算符重载,则会编译报错。
2.运算符重载是具有特名字的函数,他的名字是由operator和后面要定义的运算符共同构成。和其他函数一样,它也具有其返回类型和参数列表以及函数体。
3.重载运算符函数的参数个数和该运算符作用的运算对象数量一样多。一元运算符有一个参数,二元运算符有两个参数,二元运算符的左侧运算对象传给第一个参数,右侧运算对象传给第二个参数。
Class Date
{
public:
Date operator+(int day)
{
//a+b是由两个操作数组成
//+重载的话也就是需要,this指针和另一个操作数组成
//返回一个Date对象,方便连续使用
}
Date operator-(int day)
{
//...
}
Date& operator+=(){}
Date& operator-=(){}
//....还有好多不一一列举了
private:
int _year;//年
int _month;//月
int _day;//日
}
4.如果一个重载运算符函数是成员函数,则它的第一个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数比运算对象少一个。
5.运算符重载以后,其优先级和结合性与对应的内置类型运算符保持一致。
6.不能通过连接语法中没有的符号来创建新的操作符:比如operator@。
7.“.*” “::” “sizeof” “?:” “.” 注意以上5个运算符不能重载。
8.重载操作符至少有一个类类型参数,不能通过运算符重载改变内置类型对象的含义。
9.一个类需要重载哪些运算符,是看哪些运算符重载后有意义,比如Date类重载operator-就有意义,但是重载operator+就没有意义。
10.重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,无法很好的区分。C++规定,后置++重载时,增加一个int形参,跟前置++构成函数重载,方便区分。
Class Date
{
public:
Date operator++()
{
//前置++
//...
}
Date operator++(int)//这里的int,并不接收参数
{
//后置++
//...
}
private:
int _year;//年
int _month;//月
int _day;//日
}
11.重载<<和>>时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第一个形参位置,第一个形参位置是左侧运算对象,调用时就变成了 对象<<cout,不符合使用习惯和可读性。重载为全局函数把ostream/istream放到第一个形参位置就可以了,第二个形参位置当类类型对象。
Class Date
{
/*ostream& operator<<(ostream& out,const Date& d)
{
//...
//写成成员函数默认第一个值为This指针
//使用的方式则为 d1<<cout,不符合我们的使用习惯
//需要写成全局的,并且需要声明友元
}
*/
friend ostream& operator<<(ostream& out,const Date& d);
friend istream& operator>>(istream& in, Date& d);
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out,const Date& d)
{
//...
//返回值为ostream方便连续赋值
}
istream& operator>>(istream& in, Date& d)
{
//...
//同理
}
8.2 赋值运算符重载
赋值运算符重载是一个默认成员函数,用于完成两个已经存在的对象直接的拷贝赋值,这里要注意跟拷贝构造区分,拷贝构造用于一个对象拷贝初始化给另一个要创建的对象。
赋值运算符重载的特点:
1. 赋值运算符重载是一个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成const 当前类类型引用,否则会传值传参会有拷贝。
2. 有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是为了支持连续赋值场景。
3. 没有显式实现时,编译器会自动生成一个默认赋值运算符重载,默认赋值运算符重载行为跟默认构造函数类似,对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造。
4. 像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的赋值运算符重载就可以完成需要的拷贝,所以不需要我们显示实现赋值运算符重载。像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器自动生成的赋值运算符重载完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。像MyQueue这样的类型内部主要是自定义类型Stack成员,编译器自动生成的赋值运算符重载会调用Stack的赋值运算符重载,也不需要我们显示实现MyQueue的赋值运算符重载。这里还有一个小技巧,如果一个类显示实现了析构并释放资源,那么他就需要显示写赋值运算符重载,否则就不需要。
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
cout << " Date(const Date& d)" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
// 传引用返回减少拷贝
// d1 = d2;
Date& operator=(const Date& d)
{
// 不要检查自己给自己赋值的情况
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
// d1 = d2表达式的返回对象应该为d1,也就是*this
return *this;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
9. 取地址运算符重载
9.1 const成员函数
将const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表的后面。
const实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。const 修饰Date类的Print成员函数,Print隐含的this指针由Date* const this 变为const Date* const this。
class Date
{
public:
Date(Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// void Print(const Date* const this) const
//非const对象,也能调用const对象,是种权限的缩小
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
9.2 取地址运算符重载
取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,一般这两个函数编译器自动生成的就可以够我们用了,不需要去显示实现。除非一些很特殊的场景,比如我们不想让别人取到当前类对象的地址,就可以自己实现一份,胡乱返回一个地址。
class Date
{
public :
Date* operator&()
{
return this;
// return nullptr;
}
const Date* operator&()const
{
return this;
// return nullptr;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};
10. 类型转换
C++支持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数。
构造函数前面加explicit就不再支持隐式类型转换。
class A
{
public:
//explicit A(int a) 不允许类型转换
A(int a)
:_a1(a)//初始化列表
{}
private:
int _a1;
};
int main()
{
A aa(1);
//隐式类型转换
//首先是调用构造函数,用2的值构造个A类型的临时对象
//再拷贝构造给aa1
A aa1 = 2;
A& raa = aa1;
const A& raa1 = 2;//构建的临时对象具有常性,加const限制权限大小
//对于多参数的就可以,针对C++11
//A aa2 ={ 1, 2 };
return 0;
}
11. static成员
用static修饰的成员变量,称之为静态成员变量,静态成员变量一定要在类外进行初始化。
静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。
class A
{
public:
A(){}
private:
static int _a1;//类里面声明
//static int _a1 = 1;这里是给初始化列表的,但是static不走初始化列表
};
int A::_a1 = 1;//类外面初始化
用static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针。
静态成员函数中可以访问其他的静态成员,但是不能访问非静态的,因为没有this指针。
非静态的成员函数,可以访问任意的静态成员变量和静态成员函数。
class A
{
public:
A(){}
static int Geta1()
{
//_a2++ 这里调用不了,因为static函数没有this指针
return _a1;
}
int Print()
{
cout << Geta1() << endl;//非静态函数可以调用静态函数
}
private:
static int _a1;//类里面声明
int _a2 = 2;
};
int A::_a1 = 1;//类外面初始化
突破类域就可以访问静态成员,可以通过类名::静态成员 或者 对象.静态成员 来访问静态成员变量和静态成员函数。
静态成员也是类的成员,受public、protected、private 访问限定符的限制。
静态成员变量不能在声明位置给缺省值初始化,因为缺省值是个构造函数初始化列表的,静态成员变量不属于某个对象,不走构造函数初始化列表。
12. 友元
友元提供了一种突破类访问限定符封装的方式,友元分为:友元函数和友元类,在函数声明或者类声明的前面加friend,并且把友元声明放到一个类的里面。
外部友元函数可访问类的私有和保护成员,友元函数仅仅是一种声明,他不是类的成员函数。
class A
{
public:
friend void func1();//友元函数声明
private:
int _a1;
int _a2;
};
void func1()//函数在类外面定义
{
//...
}
友元函数可以在类定义的任何地方声明,不受类访问限定符限制。
一个函数可以是多个类的友元函数。
友元类中的成员函数都可以是另一个类的友元函数,都可以访问另一个类中的私有和保护成员。
class A
{
public:
friend class B;//声明友元类
private:
int _a1;
int -a2;
};
class B
{
public:
void funcB(const A& aa)
{
cout<<aa._a1<<endl;//调用A类
}
private:
int _b1;
int _b2;
};
友元类的关系是单向的,不具有交换性,比如A类是B类的友元,但是B类不是A类的友元。
友元类关系不能传递,如果A是B的友元, B是C的友元,但是A不是C的友元。
有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
13. 内部类
如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,跟定义在全局相比,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。
class A
{
public:
class B
{
private:
int _b1;
};
private:
int _a1;
};
int main()
{
sizeof(A);//这里只有4个字节,只包含了_a1,并没有_b1
return 0;
}
内部类默认是外部类的友元类。
内部类本质也是一种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使用,那么可以考虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其他地方都用不了。
14. 匿名对象
用 类型(实参) 定义出来的对象叫做匿名对象,相比之前我们定义的 类型 对象名(实参) 定义出来的叫有名对象。
匿名对象生命周期只在当前一行,一般临时定义一个对象当前用一下即可,就可以定义匿名对象。
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
int main()
{
A aa1;
// 不能这么定义对象,因为编译器无法识别下面是一个函数声明,还是对象定义
//A aa1();
// 但是我们可以这么定义匿名对象,匿名对象的特点不用取名字,
// 但是他的生命周期只有这一行,我们可以看到下一行他就会自动调用析构函数
A();
A(1);
A aa2(2);
return 0;
}