C++学习笔记(11)
学习是一件任重而道远的事情,与其焦虑不如动手起来,借助平台记录自己学习笔记,希望和大家多多交流,今天又是努力成为程序媛的一天!
17.类和对象
17.3 C++对象模型和this指针
17.3.1 成员变量和成员函数分开存储
在C++中,类内成员变量和成员函数分开存储
只有非静态成员变量才属于类的对象上
#include<iostream>
using namespace std;
class Person {
int A = 10; //非静态成员变量 属于类的对象上
static int B; //静态成员变量 不属于类的对象上
static void C() {}; //静态成员函数 不属于类的对象上
void D() {};//非静态成员函数 不属于类的对象上
};
int Person::B = 12;
void calculate() {
Person p;
cout << "size of p = " << sizeof(p) << endl;//空对象占用的字节大小为1 //size of p = 1
//C++编译器会给每一个空对象分配一个字节空间 是为了区分空对象占内存的位置
//每个空对象也应该有一个独一无二的内存地址
}
void A() {
Person p;
cout << "size of p = " << sizeof(p) << endl; //size of p = 4
}
int main() {
//calculate();
A(); //只有一个int非静态成员变量时对象所占内存空间为4个字节大小
//加上一个静态成员变量B对象依旧只有4个字节大小
//再加上一个静态成员函数依旧4个字节大小
//再加上一个非静态成员函数依旧4个字节大小
system("pause");
return 0;
}
17.3.2 this 指针概念
- 每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型对象会共用一块代码
那么这一块代码如何区分哪个对象调用自己呢?
C++通过提供特殊的对象指针,this指针,解决上述问题,this指针指向被调用的成员函数所属对象。
this指针是隐含每一个非静态成员函数内的一种指针
this指针不需要定义,直接使用即可
this指针用途:
- 当形参与成员变量同名时,可用this指针区分
- 在类的非静态成员函数中返回对象本身,可使用return *this
#include<iostream>
using namespace std;
class Person {
public:
//1.解决名称冲突问题
Person(int age) {
this->age = age;//this指向被调用的成员函数所属对象
}
int age;//函数参数与类成员属性同名时,会有冲突,结果不对 要么名字修改不同 要么加this指向被调用的成员函数所属对象
Person& PersonAddAge(Person& p) {//这里返回值加了引用 才是其本体返回 不加引用返回的则是副本 即以值传递的方式返回
this->age += p.age;
//this是指向p2的指针变量 *this即返回p2本体
return *this;
}
};
void test01() {
Person p(18);
cout << "age = " << p.age << endl;//结果为18
}
//返回对象本身用this
void test02() {
Person p1(20);
Person p2(10);
//若返回的是对象,这样可以链式使用对象属性
//链式编程思想
p2.PersonAddAge(p1).PersonAddAge(p1).PersonAddAge(p1);//如果去掉引用以值传递的方式返回 因为栈区数据在函数调用完之后都会销毁 所以每次返回得到的都是一个新的不同的副本 自然不能用p2的属性 结果为30
cout << "p2.age = " << p2.age << endl;//结果为70
}
int main() {
test01();
test02();
system("pause");
return 0;
}
17.3.3 空指针访问成员函数
C++中空指针也可以调用成员函数的,但是也要注意有没有用到this指针,如果用到this指针,需要加以判断保证代码的健壮性
#include<iostream>
using namespace std;
class Person {
public:
Person(int a) :m_a(a) {
cout << "析构函数的调用" << endl;
}
void show1() {
cout << "This is show1()" << endl;
}
void show2() {
//这里访问成员属性时默认使用的是this指针 在属性前面都默认加了this 等价于
//cout << "This is show2()" << "m_a =" << this->m_a << endl;//this 指向被调用的成员函数所属对象 而这里并没有指向创建任何对象 是一个空的值 无法访问里面的成员 所以会出错
//加判断,如果是指针指向为空就退出程序 不是空 就继续执行下面语句 防止程序崩溃
if (this == NULL) {
return;//退出函数
}
cout << "This is show2()" << "m_a =" << m_a << endl;
}
int m_a;
};
void test01() {
Person p(18);
p.show1();
p.show2();
}
void test02() {
//空指针也可以访问成员函数,然而其不能使用this指针
Person* p2 = NULL;
p2->show1();
p2->show2();
}
int main() {
test01();
test02();
cout << "空指针调用成员函数" << endl;
system("pause");
return 0;
}
17.3.4 const修饰成员函数
常函数:
- 成员函数后加const后我们称这个函数为常函数(本质是this指向的对象的值也不可修改)
- 常函数内不可以修改成员属性
- 成员属性声明时加关键字mutable后,在常函数中依旧可以修改
常对象:
- 声明对象前加const就称该对象为常对象
- 常对象只能调用常函数
#include<iostream>
using namespace std;
//常函数和常对象
class Person {
public:
Person(int a,int b) :m_a(a) ,m_b(b){
}
//成员函数后加const后我们称这个函数为常函数
//this本质是指针常量!即this指针的指向是不可以修改的
//const Person * const this 限定this指向和this指向的值都不可修改 第一个const相当于常函数后面的const
//成员函数后加const 修饰的是this指向 除了this指向不变,让指针指向的值也不可修改
void show() const{
if (this == NULL) {
return;//退出函数
}
this->m_b = 22;
//this->m_a = 100;//常函数不允许修改指针指向的值 报错
//this = NULL; //报错 this指针已经指向了调用此函数的p,无法再指向空 指向不可修改
cout << "This is show2()" << "m_a =" << m_a << endl;
cout << "This is show2()" << "m_b =" << m_b << endl;
//等价于cout << "This is show2()" << "m_a =" << this->m_a << endl;
}
//对于普通成员函数来说,对象的成员属性值是可以修改的 即this指向的对象的值可以更改,等价于Person * const this
void t() {
m_a = 200;
cout << "t m_a = " << m_a << endl;
cout << "t m_b = " << m_b << endl;
}
int m_a;
mutable int m_b;//特殊变量 即使在常函数中 也可以修改这个值 加关键字mutable
};
void test01() {
Person p(12,13);
p.show();
p.t();
}
void test02() {
const Person p2(15,16);//常对象
//p2.m_a = 1;//常对象的普通属性也无法修改
p2.m_b = 2;//在常对象下也可修改特殊值m_b
//常对象只能调用常函数
p2.show();
//p2.t();//常对象本身不允许修改属性 但是调用普通函数里可以修改属性值 这样就与自身设定矛盾
}
int main() {
test01();
test02();
cout << "const 修饰成员函数" << endl;
system("pause");
return 0;
}
结果为:
17.4 友元
在程序中,有些私有属性,也想让类外的特殊函数或类进行访问,就需要用到友元的技术。
友元就是声明一些特殊的函数或者类作为另一个类的好朋友,来访问到这类里的私有成员。
友元的关键字为friend,友元的三种实现:
- 全局函数做友元
- 类做友元
- 成员函数做友元
1.全局函数做友元
#include<iostream>
using namespace std;
#include<string>
class Building {
//goodFriend全局函数是Building好朋友 可以访问Building中私有成员
friend void goodFriend(Building &p);//全局函数做友元
public:
//构造函数
Building() {
m_SittingRoom = "客厅";
m_BedRoom = "卧室";
}
public:
string m_SittingRoom;
private:
string m_BedRoom;
};
//全局函数做友元
void goodFriend(Building &p) {
cout << "全局函数 m_SittingRoom " << p.m_SittingRoom << endl;
cout << "全局函数 m_BedRoom " << p.m_BedRoom << endl;
}
void test() {
Building B;
goodFriend(B);
}
int main() {
test();
system("pause");
return 0;
}
2.类做友元
#include<iostream>
using namespace std;
#include<string>
//class Building;//如果先调用Building类但是未出现需要声明才不会报错 不过Building的属性和函数要在其他类使用前写 否则没有对象属性和行为 或者把其他类函数写在调用类外成员属性函数之下
class Building {
friend class GoodFriend;//类做友元!
public:
Building();//该函数在内外写
public:
string m_SittingRoom;
private:
string m_BedRoom;
};
//类外写成员函数 加个作用域
Building::Building() {
m_SittingRoom = "客厅";
m_BedRoom = "卧室";
}
//类做友元
//一个类去访问另一个类的私有属性
class GoodFriend {
public:
GoodFriend(){
p = new Building;//new 数据类型 返回的是指针 在堆区创建 定义*b的时候并没有分配内存
//只有执行new后才会分配内存 传指针参数传过去的就是4个字节 如果用对象,参数传递占用的资源就太了
}
void visit() {
cout << "正在访问m_SittingRoom =" << p->m_SittingRoom << endl;
cout << "正在访问m_BedRoom =" << p->m_BedRoom << endl;
}
private:
Building* p;//创建类指针
};
void test() {
GoodFriend g;
g.visit();
}
int main() {
test();
system("pause");
return 0;
}
3.成员函数做友元
#include<iostream>
using namespace std;
#include<string>
class Building;//如果先调用Building类但是未出现需要声明 不过Building的属性和函数要在其他类使用前写 否则没有对象属性和行为 或者把其他类函数写在调用类外成员属性函数之下 如这里下面的写法
//成员函数做友元
//一个类去访问另一个类的私有属性
class GoodFriend {
public:
GoodFriend();
void visit();//让visit函数可以访问Building的私有属性
void visit1();//visit1函数不可以访问Building的私有属性
private:
Building* p;//创建类指针
};
class Building {
friend void GoodFriend::visit();//让成员函数做友元
public:
Building();//该函数在内外写
public:
string m_SittingRoom;
private:
string m_BedRoom;
};
//类外写成员函数 加个作用域
Building::Building() {
m_SittingRoom = "客厅";
m_BedRoom = "卧室";
}
GoodFriend::GoodFriend() {
p = new Building;//new 数据类型 返回的是指针 在堆区创建 定义*b的时候并没有分配内存
}
void GoodFriend::visit() {
cout << "正在访问m_SittingRoom =" << p->m_SittingRoom << endl;
cout << "正在访问m_BedRoom =" << p->m_BedRoom << endl;
}
void GoodFriend::visit1() {
cout << "正在访问m_SittingRoom =" << p->m_SittingRoom << endl;
//cout << "正在访问m_BedRoom =" << p->m_BedRoom << endl;//不是友元 无法访问私有属性
}
void test() {
GoodFriend g;
g.visit();
g.visit1();
}
int main() {
test();
system("pause");
return 0;
}
17.5 运算符重载
概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型
17.5.1 加号运算符重载
作用:实现两个自定义数据类型相加的运算
#include<iostream>
using namespace std;
#include<string>
//实现两个自定义数据类型相加的运算
class Person {
public:
成员函数重载+号运算
//Person operator +(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;
};
//2.全局函数重载+号运算符
Person operator+(Person & p1, Person & p2) {
Person temp;
temp.m_a = p1.m_a + p2.m_a;
temp.m_b = p1.m_b + p2.m_b;
return temp;
}
//运算符函数重载
Person operator+(Person& p1, int &num) {//引用要初始化
Person temp;
temp.m_a = p1.m_a + num;
temp.m_b = p1.m_b + num;
return temp;
}
void T() {
Person p1;
p1.m_a = 10;
p1.m_b = 10;
Person p2;
p2.m_a = 20;
p2.m_b = 20;
//成员函数重载本质调用
//Person p3 = p1.operator+(p2);
全局函数重载本质调用
//Person p3 = operator+(p1, p2);都可运行成功
Person p3 = p1 + p2;
//运算符重载 也可以发生函数重载
int a = 100;
Person p4 = p1 + a;
cout << "p3.m_a = " << p3.m_a << endl;
cout << "p3.m_b = " << p3.m_b << endl;
}
int main() {
T();
system("pause");
return 0;
}
注意:
- 对于内置的int…等数据类型的表达式的运算符运算是不能修改的
- 不要滥用运算符重载,比如+号运算,里面放-号等杂乱运算,行为迷惑
17.5.2 左移运算符重载
作用:可以输出自定义数据类型
#include<iostream>
using namespace std;
#include<string>
class Person {
friend ostream& operator<<(ostream& cout, Person& p);
public:
Person(int a, int b) :m_a(a), m_b(b) {
}
利用成员函数重载 左移运算符 1号
//void operator <<(ostream &cout) {//cout是标准的输出流类对象 而且全局只能有一个 用引用的方式传过来 不能创建一个新的 由于不能简化为p << cout形式 所以要采用全局函数重载
// cout << this->m_a << endl;
//}
成员函数重载 左移运算符 2号
//void operator <<(Person& p) {
// cout << "m_a = " << p.m_a << " m_b = " << p.m_b;
//}
private:
int m_a;
int m_b;
};
ostream& operator<<(ostream& out, Person& p) {//cout是标准的输出流类对象 而且全局只能有一个 用引用的方式传过来 不能创建一个新的 返回也要引用的方式 这样没有复制返回值 而是返回对象的引用即对象本身
out << "m_a = " << p.m_a << " m_b = " << p.m_b;
return out;// cout << p是一个函数的调用,返回值是void,需要返回cout类型才能加endl; 这样cout<<每次返回就是输出流cout 了 就可以使用其特点输出了 形成链式编程思想。
}
void test01() {
Person p(5, 10);
cout << p << endl;//想输出p就返回p的所有属性
//p << cout;//如果是成员函数重载 调用的时候简化成该种形式 并不是我们想调用的cout << p形式
//p << (p);//成员函数重载又或许是这种形式 但显然不是想要的形式 == p.operator<<(p);
}
int main() {
test01();
system("pause");
return 0;
}
备用参考链接:左移运算符函数重载
补充:我这里用的是速览定义也可,得到cout是ostream输出流自定义类的对象
17.5.3 递增运算符重载
作用:通过重载递增运算符,实现自己的整型数据
补充:运算符的优先级顺序
#include<iostream>
using namespace std;
class MyIntger {
friend ostream& operator<<(ostream& out, MyIntger myint);
public:
MyIntger() {
m_a = 0;
}
//重载前置++运算符
MyIntger& operator++() {//先运算++,再<< ; ++如果返回的是void就不满足<<里的参数设置了 所以需要返回MyIntger类型
//并且要加&引用返回本体 因为如果原本++(++i) 返回值是2 本身返回i也是2 如果不是引用返回而是值传递++(++MyIntger)结果为2 但是MyIntger值为1 因为操作的不是本体
//先加1
m_a++;
//后返回自增值
return *this;
}
//重载后置++运算符
const MyIntger operator++(int) { //int 纯属让编译器区分 是后置 无参的是前置
//不返回引用是因为不能返回函数局部变量 否则再次访问就是非法操作
//先给出目前的值
MyIntger temp = *this;
//自增
m_a++;
//返回当前值
return temp;
}
private:
int m_a;
};
ostream& operator<<(ostream& out, MyIntger myint) {
cout << myint.m_a << endl;
return out;
}
void test() {
MyIntger myint;
cout << myint << endl;//0
cout << ++myint << endl;//先提示没有可以这样运算的左运算符 所以要先函数重载左运算符 //1
cout << ++myint << endl;//2
cout << myint << endl;//2
cout << myint++ << endl;//2
cout << myint << endl;//3
cout << ++(++myint) << endl;//5
cout << myint << endl;//5
//cout << (myint++)++ << endl;//不加const返回5 其实只增加了1,因为第二次自增作用在一个临时对象上 所以返回临时对象5 两次不是同一个作用对象 无论多少次后置递增(递减)都只相当于运算一次
//cout << (myint++)++ << endl;//加const报错
}
int main() {
test();
system("pause");
return 0;
}
注意:
参考链接:C++之运算符重载(前置++和后置++)
- const MyIntger operator++(int)后置++运算符的返回类型为什么要是const对象呢?
-
有两个原因:
- 1.如果不是const对象,myint(++)++这样的表达式就可以通过编译。但是,其效果却违反了我们的直觉 。myint其实只增加了1,因为第二次自增作用在一个临时对象上。
- 2.对于内置类型,(i++)++这样的表达式是不能通过编译的。自定义类型的操作符重载,应该与内置类型保持行为一致 。myint++的返回类型如果改成非const对象,肯定能通过编译,但是我们最好不要这样做
-
- 前置++的返回类型是MyIntger&,后置++的返回类型const MyIntger。这意味着,前置++返回的是左值,后置++返回的是右值。
- 对于什么是左值,右值:
- ++i是直接给i变量加1,然后返回i本身,因为i是变量,所以可以被赋值,因此是左值表达式
- i++是先产生一个临时变量,记录i的值,在i的值被使用后,再后给i加1,接着返回临时变量,然后临时变量不存在了,所以,不能再被赋值,因此是右值表达式
- 对于什么是左值,右值:
- 前置++的效率更高,应优先使用前置++,尤其是对于用户自定义类型的自增操作。理由是:后置++会生成临时对象。temp是一个临时对象,会造成一次构造函数和一次析构函数的额外开销。虽然,编译器在某些情况下可以优化掉这些开销,但是,我们最好不要依赖编译器的行为。
同理:写出前置–和后置–函数重载
#include<iostream>
using namespace std;
class MyIntger {
friend ostream& operator<<(ostream& cout,const MyIntger& myint);
public:
MyIntger() {
num = 0;
}
//前置--
MyIntger& operator--() {
//先运算
this->num--;
//后返回值本身
return *this;
}
//后置--
const MyIntger operator--(int) {//加const表示返回值的内容不可改动 即不能作为左值 int 为占位参数
//先临时定义现在的值
MyIntger temp = *this;
//递减操作
num--;
//返回递减之前的值
return temp;
}
private:
int num;
};
ostream& operator<<(ostream& cout,const MyIntger& myint) {//全局函数 左移运算符函数重载
cout << myint.num << endl;
return cout;
}
void test() {
MyIntger myint;
cout << myint << endl;//0
cout << --myint << endl;//-1
cout << myint << endl;//-1
cout << --(--myint) << endl;//-3
cout << myint-- << endl;//起初这行报错 把<< 后面的参数加上const就行 //-3
cout << myint << endl;//-4
}
int main() {
test();
system("pause");
return 0;
}
这里在后置–的时候出现一个问题,:
当然我这里是–,不过搜素一番发现后置++也会这样,原因:后置++返回值类型不是引用,是一个临时值,在返回之后就会消亡,但是非const引用(参考链接:C++禁止引用非const的临时变量)需要对象一直存在,临时值则不行,也就无法调用这个函数;当然也可以把引用和const都去掉,值传递也可,而临时变量是可以作为常量引用的,因为临时变量本身是可以修改的。
17.5.5 赋值运算符重载
如果类中有属性指向堆区,做赋值操作时也会出现深浅拷贝问题。
如果在堆区创建了变量,会出现堆区空间重复释放的问题,需要深拷贝解决,往期笔记:深拷贝和浅拷贝
#include<iostream>
using namespace std;
class Person {
public:
Person(int a) {
num = new int(a);//析构函数 堆区创建变量
}
~Person() {
//判断 并删除堆区创造变量
if (num != NULL) {
delete num;
}
}
//深拷贝 赋值运算符
Person& operator=(Person& p) {
//编译器默认浅拷贝操作:
// num = p.num;
//先释放干净堆区数据
if (num != NULL) {
delete num;
}
//进行深拷贝
num = new int(*p.num);
//返回自身
return *this;
}
//private:
int *num;//创建类指针
};
void test() {
Person p1(18);//函数执行完就释放了
cout << *p1.num << endl;//18
Person p2(20);
Person p3(16);
cout << *p2.num << endl;//20
p1 = p2;//一旦进入赋值操作 p1就把地址数值等复制一本给p2 这样两个就指向同一个地址 析构两次同地址发生了错误
cout << *p2.num << endl;//20
p1 = p2 = p3;//p3赋给p2 返回p2 再把p2赋给p1
cout << "p1.num = " << *p1.num << " p2.num = " << *p2.num << " p3.num = " << *p3.num << endl;//p1.num = 16 p2.num = 16 p3.num = 16
}
int main() {
test();
//int a1 = 10;
//int a2 = 12;
//int a3 = 15;
//a1 = a2 = a3;
//cout << a1 << " " << a2 << " " << a3 << endl;//15 15 15即内置=运算是允许连续赋值的
system("pause");
return 0;
}
17.5.6 关系运算符重载
作用:重载关系运算符,可以让两个自定义类型对象进行对比操作 == 和 !=
#include<iostream>
using namespace std;
#include<string>
//关系运算符重载 == !=
class Person {
public:
Person(string name,int age) {
m_name = name;
m_age = age;
}
bool operator==(Person& p) {//注意返回类型是bool
if (this->m_name == p.m_name && this->m_age == p.m_age) {
return true;
}
return false;
}
bool operator!=(Person& p) {
if (this->m_name == p.m_name && this->m_age == p.m_age) {
return true;
}
return false;
}
string m_name;
int m_age;
};
void test() {
Person p1("Tom", 18);
Person p2("Su", 20);
//Person p3("a", 22);
cout << "姓名:" << p1.m_name << " 年龄:" << p1.m_age << endl;
cout << "姓名:" << p2.m_name << " 年龄:" << p2.m_age << endl;
if (p1 == p2 ) {//不具有链式 原内置也没有链式
cout << "p1 == p2" << endl;
}
else {
cout << "p1 != p2" << endl;
}
}
int main() {
test();
system("pause");
return 0;
}
17.5.7 函数调用运算符重载
- 函数调用运算符()也可以重载
- 由于重载后使用方式很像函数调用 又称伪函数
- 仿函数没有固定写法,很灵活
#include<iostream>
using namespace std;
#include<string>
//函数调用重载
//打印函数调用
class MyPrintf {
public:
void operator()(string s) {//这个小括号表示对其重载 后面括号表示传入的参数
cout << s << endl;
}
};
void test01() {
MyPrintf myprintf;
myprintf("hello");//由于调用此重载类成员函数很像函数调用 所以叫伪函数 ()重载函数调用即为伪函数调用
}
//函数灵活
//加法类
class Add {
public:
int operator()(int a,int b){
return a + b;
}
};
void test02() {
Add ex;
int re = ex(12, 15);
cout << re << endl;
//匿名函数对象
//不创建对象输出
cout << Add()(20, 30) << endl;//前面Add()就是创建一个匿名对象 然后利用函数重载小括号 //匿名对象特点:当前行执行完立即释放 数据类型+()就是匿名对象 这里又有调用所以是匿名函数对象
}
//根据自己需要写仿函数 很灵活
int main() {
test01();
test02();
system("pause");
return 0;
}
解惑链接:
1.&取地址符与* 解引用操作符
2.值传递,指针传递,引用传递
3.值传递,指针传递,引用传递区别
第十一篇笔记到此结束,C++基础学习会持续更新在C++学习笔记合集中,当作学习笔记复习,如果能帮助其他小伙伴就更好了。
笔记是看黑马程序C++时做的记录,笔记中如果有错误和改进的地方,欢迎大家评论交流,up up up!!!
学习原视频来自:黑马程序员C++从0到1