文章目录
引用
基本使用
**作用: **给变量起别名
语法: 数据类型 &别名 = 原名
int a = 10;
int &b = a;
注意事项
- 引用必须初始化
- 引用在初始化后,不可以改变
int &a; //这是错误的,因为必须初始化
int &c = a; //一旦初始化后,就不可以更改
c = b; //这是赋值操作,不是更改引用
引用作为函数参数
**作用:**函数传参时,可以利用引用的技术让形参修饰实参
**优点:**可以简化指针修改实参
void func(int &a, int &b){
int tmp = a + b;
}
**注意:**在函数参数中的引用不可以传入一个临时变量,如果真的想引用这个变量,用const
来保证我们不会对临时变量的内存进行更改。
示例:
//这里重载了左移运算符和递减运算符
class myInteger{
friend ostream& operator<<(ostream &cout, const myInteger &i);
public:
myInteger(){
this->mNum = 0;
}
myInteger& operator--(){
this->mNum--;
return *this;
}
myInteger operator--(int){
myInteger tmp = *this;
this->mNum--;
return tmp;
}
private:
int mNum;
};
//由于后置递减运算符返回的是一个临时变量,
//所以左移的引用必须用const修饰
ostream& operator<<(ostream &cout, const myInteger &i){
cout << i.mNum;
return cout;
}
int main(){
myInteger m;
cout << m-- << ' ' << m << endl;
}
引用做函数返回值
注意:不要返回局部变量引用
//返回局部变量引用
int& test01() {
int a = 10; //局部变量
return a;
}
//不能返回局部变量的引用,
//局部变量释放后编译器只回对其保留一次,之后就是乱码
引用的本质
本质:引用的本质在c++内部实现是一个指针常量. `int* const ref = &a;
int main(){
int a = 10;
//自动转换为 int* const ref = &a; 指针常量是指针指向不可改,也说明为什么引用不可更改
int& ref = a;
ref = 20; //内部发现ref是引用,自动帮我们转换为: *ref = 20;
}
常量引用
作用: 常量引用主要用来修饰形参,防止误操作
在函数形参列表中,可以加const修饰形参,防止形参改变实参
int main() {
//int& ref = 10; 引用本身需要一个合法的内存空间,因此这行错误
//加入const就可以了,编译器优化代码,int temp = 10; const int& ref = temp;
const int& ref = 10;
}
构造函数与析构函数
- 构造函数:用于初始化对象。
- 语法:Name(){};
- 没有类型,但是可以有参数,所以可以重构。
- 系统默认给一个空的默认构造函数。
- 可以用参数进行重构,只要参数数量、类型、顺序不一即可。
- 程序在对象申请时自动调用,且只调一次。
- 析构函数:用于释放初始化时申请的堆区内存。
- 语法:~Name(){};
- 没有类型也没有参数,故不可重构。
- 系统也是默认给个空的函数。
- 程序在对象销毁之前自动调用,且只调一次。
注意:
- 函数写在public里。
构造函数的类型及调用
两种分类方式:
- 按参数:有参构造和无参构造。
- 按类型:普通构造和拷贝构造。
三种调用方式:
- 括号法、显式调用、隐式调用。
拷贝构造函数的写法
Name(const Name &p){
// 对一些值进行拷贝
}
三种调用方式
- 括号法
- Name p1(10);
- 直接在创建的对象后面打括号写上参数,但是执行默认构造时不能打括号,因为
Name p()
会被认为是一种函数。
- 显示调用
- Name p2 = Name(p1);
- 单独的
Name(p1)
会被视作给一个匿名对象进行构造,在这句语句执行完之后立即执行析构。
- 隐式调用
- Name p3 = 10;
注意:
- 不可以用 拷贝构造函数 初始化匿名对象,编译器会认为这是一个对象的声明。
- Name (p3) == Name p3;
拷贝构造函数调用的时机
- 使用一个已经创建完毕的对象来初始化一个新对象
-
Name p(100); Name p1 = p; Name p3; p3 = p; // 不是调用拷贝函数,而是赋值
-
- 值传递的方式给函数参数传值
-
void doWork(Name p1){} void main(){ Name p; doWork(p); }
- 此处其实是调用拷贝函数复制了一个类过去,而并非原来那个。
-
- 以值方式返回局部对象
-
Name doWork(){ Name p; return p; }
- 此时,因为p是局部对象,所以在函数执行完之后就销毁了,于是编译器用拷贝函数复制了一个新对象返回。
-
构造函数的调用规则
默认情况下,编译器提供给每个类三个函数:
- 默认构造函数
- 默认析构函数
- 默认拷贝函数
调用规则:
- 你写了构造函数,默认提供你析构和拷贝函数。
- 你写了析构,提供拷贝,不提供构造。
- 你写了拷贝,其他两个都不提供。
深拷贝与浅拷贝
- 浅拷贝:简单的赋值拷贝操作。
- 深拷贝:在堆区重新申请空间,进行拷贝操作。
简单来说,对于一些数值来说,我们直接给对应属性拷贝过去是可以的。但是对于一些地址来说,只拷贝地址数值是不行的,因为对于这两个对象的释放来说,这两个属性所指向的地址是相同的,所以会造成二次释放的问题,所以我们就需要在拷贝的时候在堆区申请一个新空间。
总结:如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题
初始化列表
class Person{
public:
//传统方式初始化
/*Person(int a, int b, int c) {
m_A = a;
m_B = b;
m_C = c;
}*/
//初始化列表
Person(int a, int b, int c) :m_A(a), m_B(b), m_C(c) {
}
private:
int m_A;
int m_B;
int m_C;
}
类对象作为类成员
一个类对象作为另一个类对象的成员时:
class B{
public:
int name;
}
class A{
public:
B b;
}
构造时:B先构造,A后构造。
析构时:A先析构,B后析构。
静态成员
- 静态成员变量
- 所有对象共享同一份数据
- 在编译阶段分配内存
- 类内声明,类外初始化
- 静态成员函数
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量
- 静态成员函数也有权限,私有类外不可访问
示例1 : 静态成员变量
class person{
public:
static int ma; //静态成员变量
int mb;
//静态成员函数
static void func(){
cout << "静态成员函数调用" << endl;
ma = 10;
//mb = 10; //错误,不可以访问非静态成员变量
}
};
int person::ma = 1; //类外声明
int main(){
//静态成员变量两种访问方式
//1.通过对象
person a;
cout << a.ma << endl;
//2.通过类名
cout << person::ma << endl;
}
示例2: 静态成员函数
class Person
{
public:
//静态成员函数特点:
//1 程序共享一个函数
//2 静态成员函数只能访问静态成员变量
static void func()
{
cout << "func调用" << endl;
m_A = 100;
//m_B = 100; //错误,不可以访问非静态成员变量
}
static int m_A; //静态成员变量
int m_B; //
private:
//静态成员函数也是有访问权限的
//只能类内访问,类外访问不到
static void func2()
{
cout << "func2调用" << endl;
}
};
int Person::m_A = 10;
int main(){
Person::func2(); //私有权限访问不到
}
C++对象模型和this指针
成员变量和成员函数分开存储
- 在C++中,类内的成员变量和成员函数分开存储
- 只有非静态成员变量才属于类的对象上
class Person {
public:
Person() {
mA = 0;
}
//非静态成员变量占对象空间
int mA;
//静态成员变量不占对象空间
static int mB;
//函数也不占对象空间,所有函数共享一个函数实例
void func() {
cout << "mA:" << this->mA << endl;
}
//静态成员函数也不占对象空间
static void sfunc() {
}
};
//空类
class case{
public:
};
void test01(){
cout << sizeof(Person) << endl; //输出为4,就是一个int大小
}
void test02(){
cout << sizeof(case) << endl;
//输出为1,因为编译器会自动分配一字节空间,为了区分空对象占内存的位置
}
int main() {
test01();
test02();
system("pause");
return 0;
}
this指针
- 当形参和成员变量同名时,可用this指针来区分
- 在类的非静态成员函数中返回对象本身,可使用return *this
class Person
{
public:
Person(int age)
{
//1、当形参和成员变量同名时,可用this指针来区分
this->age = age;
}
Person& PersonAddPerson(Person p)
{
this->age += p.age;
//返回对象本身
return *this;
}
int age;
};
空指针访问成员函数
- C++中空指针也是可以调用成员函数的,但是也要注意有没有用到this指针
class Person {
public:
void ShowClassName() {
cout << "我是Person类!" << endl;
}
void ShowPerson() {
if (this == NULL) {
return;
}
cout << mAge << endl;
}
public:
int mAge;
};
void test01()
{
Person * p = NULL;
p->ShowClassName(); //空指针,可以调用成员函数
p->ShowPerson(); //但是如果成员函数中用到了this指针,就不可以了
}
const修饰成员函数
常函数:
- 成员函数后加const后我们称为这个函数为常函数
- 常函数内不可以修改成员属性
- 成员属性声明时加关键字mutable后,在常函数中依然可以修改
常对象:
- 声明对象前加const称该对象为常对象
- 常对象只能调用常函数
示例:
class Person {
public:
Person() {
m_A = 0;
m_B = 0;
}
//this指针的本质是一个指针常量,指针的指向不可修改
//如果想让指针指向的值也不可以修改,需要声明常函数
void ShowPerson() const {
//const Type* const pointer;
//this = NULL; //不能修改指针的指向 Person* const this;
//但是this指针指向的对象的数据是可以修改的(函数不加const情况下)
//this->mA = 100;
//const修饰成员函数,表示指针指向的内存空间的数据不能修改,除了mutable修饰的变量
this->m_B = 100;
}
void MyFunc() const {
//mA = 10000; //错误,不可修改
}
void func2(){
mA = 100;
}
public:
int m_A;
mutable int m_B; //可修改 可变的
};
//const修饰对象 常对象
void test01() {
const Person person; //常量对象
cout << person.m_A << endl;
//person.mA = 100; //常对象不能修改成员变量的值,但是可以访问
person.m_B = 100; //但是常对象可以修改mutable修饰成员变量
//常对象访问成员函数
person.func2(); //常对象不能调用非常函数
}
友元
- 友元的目的就是让一个函数或者类能够访问另一个类中私有成员
- 友元的关键字为 friend
- 友元的三种实现
- 全局函数做友元
- 类做友元
- 成员函数做友元
全局函数做友元
//告诉编译器 goodGay全局函数 是 Building类的好朋友,可以访问类中的私有内容
friend void goodGay(Building * building);
类做友元
//告诉编译器 goodGay类是Building类的好朋友,可以访问到Building类中私有内容
friend class goodGay;
成员函数做友元
class Building;
class goodGay
{
public:
goodGay();
void visit(); //只让visit函数作为Building的好朋友,可以发访问Building中私有内容
void visit2();
private:
Building *building;
};
class Building
{
//告诉编译器 goodGay类中的visit成员函数 是Building好朋友,可以访问私有内容
friend void goodGay::visit();
public:
Building();
public:
string m_SittingRoom; //客厅
private:
string m_BedRoom;//卧室
};
运算符重载
作用:实现两个自定义数据类型相加的运算
+加运算符的重载
class Person {
public:
Person() {};
Person(int a, int b)
{
this->m_A = a;
this->m_B = b;
}
//成员函数实现 + 号运算符重载
Person operator+(const Person& p) {
Person temp;
temp.m_A = this->m_A + p.m_A;
temp.m_B = this->m_B + p.m_B;
return temp;
}
public:
int m_A;
int m_B;
};
//全局函数实现 + 号运算符重载
//Person operator+(const Person& p1, const Person& p2) {
// Person temp(0, 0);
// temp.m_A = p1.m_A + p2.m_A;
// temp.m_B = p1.m_B + p2.m_B;
// return temp;
//}
//运算符重载 可以发生函数重载
Person operator+(const Person& p2, int val)
{
Person temp;
temp.m_A = p2.m_A + val;
temp.m_B = p2.m_B + val;
return temp;
}
void test() {
Person p1(10, 10);
Person p2(20, 20);
//成员函数方式
Person p3 = p2 + p1; //相当于 p2.operaor+(p1)
cout << "mA:" << p3.m_A << " mB:" << p3.m_B << endl;
Person p4 = p3 + 10; //相当于 operator+(p3,10)
cout << "mA:" << p4.m_A << " mB:" << p4.m_B << endl;
}
总结:对于内置的数据类型的表达式的的运算符是不可能改变的
<<左移运算符的重载
作用:可以输出自定义数据类型
class Person {
friend ostream& operator<<(ostream& out, Person& p);
public:
Person(int a, int b)
{
this->m_A = a;
this->m_B = b;
}
//成员函数实现为:p.operator<<(cout),cout在右边
//成员函数 实现不了 p << cout 不是我们想要的效果
//ostream& operator<<(ostream& cout){
cout << this->m_A << endl;
cout << this->m_B << endl;
return cout;
//}
private:
int m_A;
int m_B;
};
//全局函数实现左移重载
//ostream对象只能有一个
ostream& operator<<(ostream& out, Person& p) {
out << "a:" << p.m_A << " b:" << p.m_B;
return out;
}
void test() {
Person p1(10, 20);
cout << p1 << "hello world" << endl; //链式编程
}
++递增运算符重载
作用: 通过重载递增运算符,实现自己的整型数据
class MyInteger {
friend ostream& operator<<(ostream& out, MyInteger myint);
public:
MyInteger() {
m_Num = 0;
}
//前置++
MyInteger& operator++() {
//先++
m_Num++;
//再返回
return *this;
}
//后置++
MyInteger operator++(int) {
//先返回
MyInteger temp = *this; //记录当前本身的值,然后让本身的值加1,但是返回的是以前的值,达到先返回后++;
m_Num++;
return temp;
}
private:
int m_Num;
};
ostream& operator<<(ostream& out, const MyInteger& myint) {
out << myint.m_Num;
return out;
}
//前置++ 先++ 再返回
void test01() {
MyInteger myInt;
cout << ++myInt << endl;
cout << myInt << endl;
}
//后置++ 先返回 再++
void test02() {
MyInteger myInt;
cout << myInt++ << endl;
cout << myInt << endl;
}
注意:
- 前置后置的区别就在于前置函数内无参数,后置有一个int占位符,而且必须是int,这是规定;
- 前置返回的是引用,可以链式编程(嵌套);
- 后置返回的是类型,而且由于返回的是临时变量,编译器拷贝了一份临时变量传回,二者并非指向同一块内存,所以不能链式编程,因为这个值的类型和函数参数的类型不一致,而且最后的结果也不正确。
- 返回值是临时变量来作为另一个函数的参数,在这个函数执行完之后就销毁了;
- 如果参数是引用,那么这个变量的生命周期被延长到跟引用一致(代码块完结)。
- 引用作为参数不能接收临时变量,如果一定要接收,需要加上const来保证内存不会被修改;
- 非引用的参数可以接收临时变量。
- 返回值是临时变量来作为另一个函数的参数,在这个函数执行完之后就销毁了;
=赋值运算符的重载
c++编译器至少给一个类添加4个函数
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对属性进行值拷贝
- 赋值运算符 operator=, 对属性进行值拷贝
作用:如果类中有属性指向堆区,做赋值操作时也会出现深浅拷贝问题,需要手写=重载
示例:
class person{
public:
person(int age){
//将年龄数据开辟到堆区
this->mAge = new int(age);
}
person& operator=(const person &p){
if(this->mAge != NULL){
delete this->mAge;
this->mAge = NULL;
}
//编译器提供的代码是浅拷贝
//mAge = p.mAge;
//提供深拷贝 解决浅拷贝的问题
mAge = new int(*p.mAge);
return *this;
}
~person(){
if(this->mAge != NULL){
delete this->mAge;
this->mAge = NULL;
}
}
int *mAge;
};
int main(){
person p1(18);
person p2(20);
person p3(30);
p3 = p2 = p1; //赋值操作
cout << "p1的年龄为:" << *p1.mAge << endl;
cout << "p2的年龄为:" << *p2.mAge << endl;
cout << "p3的年龄为:" << *p3.mAge << endl;
}
==关系运算符重载
**作用:**重载关系运算符,可以让两个自定义类型对象进行对比操作
示例:
class person{
public:
person(string name, int age){
this->mName = name;
this->mAge = age;
}
bool operator==(person &p){
if(this->mName == p.mName && this->mAge == p.mAge)
return true;
return false;
}
bool operator!=(person& p){
if(this->mName == p.mName && this->mAge == p.mAge)
return false;
return true;
}
string mName;
int mAge;
};
int main(){
person p1("xj", 20);
person p2("xj", 10);
if(p1 == p2) cout << "p1 == p2" << endl;
if(p1 != p2) cout << "p1 != p2" << endl;
}
()函数调用运算符的重载
- 函数调用运算符 () 也可以重载
- 由于重载后使用的方式非常像函数的调用,因此称为仿函数
- 仿函数没有固定写法,非常灵活
示例:
class myPrint{
public:
void operator()(string s){
cout << s << endl;
}
};
class myAdd{
public:
int operator()(int a, int b){
return a + b;
}
};
int main(){
myPrint mp;
mp("xj");
//匿名对象调用
cout << myAdd()(10, 20) << endl;
}
注意:
- 匿名对象写法:
类名 + ()
- 匿名对象相比具体对象来说,在写法上只是少了个名字,其他操作一致
- 创建匿名对象时,也会调用构造函数,有参数就在括号里写
- 匿名对象的生命周期短,在作用域结束后销毁