目录
接上篇
2.8类和对象的存储
空对象占用内存空间为:1
C++编译器为了区分不同的空对象,需要给对象分配内存空间,所以在节省内存的前提下给空对象分配一个字节空间。
我们可以运行下面这段代码来测试
#include <iostream>
using namespace std;
class Person
{
};
int main(){
Person p;
cout << sizeof(p) << endl;
return 0;
}
运行结果应该是 1 ,至少我的编译器是这样。(当然末尾还有换行,考试重灾区哈哈)
那么如果类里面定义了一个int类型的成员变量呢?这时对象占用的内存是多大?
4,5,8是不同人想的答案,但正确的只有一个 4
当对象中包含成员变量时,编译器为其分配内存空间,这时便不再需要那一个字节内存
可以理解为编译器保证对象有自己独特的内存地址
为了进一步探究成员变量和成员函数在对象中占用的内存,我们可以依次输入下列语句进行测试
int a;
static int b;
void fun1(){ };
static void fun2(){ };
在逐个运行后我们可以知道结果并得出结论
#include <iostream>
using namespace std;
class Person
{
int a; // 非静态成员变量 属于类的对象上 4
static int b; // 静态成员变量 不属于类的对象上 4
void fun1(){ }; // 非静态成员函数 不属于类的对象上 4
static void fun2(){ }; // 静态成员函数 不属于类的对象上 4
};
int main(){
Person p;
cout << sizeof(p) << endl;
return 0;
}
在经过测试后我们可以发现,只有使用int a 时该程序输出改变为 4 ,也就是说只有非静态成员变量属于类的对象上,其他成员均不属于,也不会对对象的内存占用造成影响。
一句话:成员变量和成员函数是分开存储的
2.9 this指针
this指针主要有两个用途
- 解决名称冲突
- 在类的非静态成员函数中返回对象本身,可使用return *this
解决名称冲突
this指针指向 被调用的成员函数 所属的对象
class Person
{
public:
Person(int age)
{
this -> age = age; //此处左值需用this指针,否则无法表示成员变量age
}
int age;
};
这也是为什么大多数优秀程序员会选择将成员名加上前缀修饰,如age写成m_Age(m表示member),这样可以省去许多麻烦,也提高可读性。
返回对象本身用*this
考虑下面这段代码的输出
#include <iostream>
using namespace std;
class Person
{
public:
Person(int age)
{
this->age = age;
}
void Addage(Person& p)
{
this->age += p.age;
}
int age;
};
int main(){
Person p1(10);
Person p2(10);
p2.Addage(p1);
cout << p2.age << endl;
return 0;
}
显然,这段代码实现的是age的一个简单的叠加
那么我们可不可以实现累加呢?通过多次调用Addage()使得p2的age多次增加
或许可以这样做: p2.Addage(p1).Addage(p1).Addage(p1)
这样能否使得p2的age变成40呢?
实践出真知
报错了?因为Addage()返回的是一个空值,考虑原本的p2.Addage(),访问对象需要是一个对象,也就是说如果p2.Addage()的返回值仍是p2我们就能实现上述累加操作。
将Addage()函数略作修改
Person& Addage(Person& p)
{
this->age += p.age;
return *this;
}
这样我们就能实现多重套娃(bushi)
需要注意的是,定义的Addage返回值是Person&类型,是对被调用成员函数所属的对象的引用,如果用Person的话就是创建了一个新的对象,这与我们的原想法不同
结果如何读者可自行尝试
2.10空指针访问成员函数
考虑以下代码
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
void Say_hi()
{
cout << "Hi," << name << endl;
}
string name = "David";
};
int main(){
Person *p = NULL;
p->Say_hi();
return 0;
}
该程序的输出结果为 Hi, (部分编译器会直接报错)
这是因为在Say_hi()中调用了name,这实际上是this->name,由于p是空指针而非指向某个对象,所以运行结果不会是 Hi,David
我们可以修改Say_hi()函数,以避免后期因为传入空指针导致程序出错
void Say_hi()
{
if(this == NULL)
{
return;
}
cout << "Hi," << name << endl;
}
2.11 const修饰成员函数
在类与对象中有常函数与常对象
常函数:
- 成员函数后加const后我们称这个函数为常函数
- 常函数内不可以修改成员属性
- 成员属性声明时可加关键字mutable,在常函数中依然可以修改
考虑下面这段代码
#include <iostream>
using namespace std;
class Person
{
public:
void show() const
{
this->m_A = 1;
this->m_B = 2;
cout << m_A << ' ' << m_B << endl;
}
int m_A = 0;
mutable int m_B;
};
int main(){
Person p1;
p1.show();
return 0;
}
测试后可以发现,编译器报错了,原因是在常函数show()中尝试修改成员变量m_A
在将这行语句(this->m_A = 1)注释掉后我们可以得到结果为 0 2
所以在成员函数后加const,本质上是修饰this指针,使其指向的值无法改变,而mutable则是创造一个特例
另外需要注意的是,this指针的本质是 指针常量 其指向不可修改
也就是说下面这段代码会报错
#include <iostream>
using namespace std;
class Person
{
public:
void show() const
{
this = NULL; //this不能作为可修改的左值
}
};
int main(){
return 0;
}
常对象:
- 声明对象前加const称该对象为常对象
- 常对象只能调用常函数
因为常对象中的成员属性不可修改,所以只能调用常函数,以确保不会修改成员属性
3.友元
3.1一点概述
友元就是声明一些特殊的函数或类,使其可以访问某类的私有成员
友元的关键字为 friend
友元的三种实现:
- 全局函数作友元
- 类作友元
- 成员函数作友元
3.2三种实现
全局函数作友元
在类中声明该函数,并在前面加上friend即可
如:friend void fun_c(int n); friend void goodGay(Building *building);
类作友元
与全局函数作友元类似,在类中friend并声明
如:friend class Building;
成员函数作友元
类似,只是语法不同
如:friend void GoodGay::visit();
4.运算符重载
概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型
4.1加号运算符重载
可以通过两种方式实现加号运算符重载
- 成员函数实现
- 全局函数实现
多说无益,上代码,看实例
#include <iostream>
using namespace std;
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;
}
void test() {
Person p1(10, 10);
Person p2(20, 20);
//成员函数方式
Person p3 = p2 + p1; //相当于 p2.operator+(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;
}
int main()
{
test();
return 0;
}
//本程序源自 黑马程序员
//找不到源码,跟着视频教学敲的
运算符重载后并没有彻底改变加号的运算法则
也就是说 cout << 1 + 2 的结果依然是 3
4.2左移运算符重载
在使用cout输出某些内容时用到的 << 就是左移运算符
重载左移运算符时只能使用全局函数,因为用成员函数无法实现cout在左侧
先上代码看实例
#include <iostream>
using namespace std;
class anupis
{
friend void test(anupis p);
friend ostream& operator<<(ostream& out, anupis& p);
public:
anupis(int a, int b)
{
m_A = a;
m_B = b;
}
private:
int m_A;
int m_B;
};
ostream& operator<<(ostream& out, anupis& p) //全局函数重构 out是形参,本质也是cout
{
cout << "a:" << p.m_A << "b:" << p.m_B;
return out; //要使得后面可以继续输出,所以得返回out
}
void test(anupis p)
{
cout << p << "Hello,World!" << endl;
}
int main()
{
anupis p(10,10);
test(p);
return 0;
}
左移运算符重载与加号运算符重载类似,主要是弄明白cout的数据类型
在Visual Studio中,我们可以通过右键cout转到定义来查看cout的定义
很明显,cout是ostream也就是输出流中的一个对象
由于iostream的拷贝构造函数不允许使用,所以我们只能传递地址而非值
这就是为什么ostream后要用&
ostream& operator<<(ostream& out, anupis& p)
{
cout << "a:" << p.m_A << "b:" << p.m_B;
return out;
}
4.3递增运算符重载
前置++运算符重载很简单
//重载前置++运算符 返回引用为了一直对一个数据进行递增操作
MyInteger& operator++()
{
//先进行++运算
m_A++;
//再将自身做返回
return *this;
}
重载后置++运算符:
MyInteger operator++(int)
{
//先记录
MyInteger temp = *this;
//后递增
m_A++;
//最后返回
return temp;
}
本来正常思路是先返回值,然后再递增,但返回值之后函数结束了就无法递增,所以得先记录值,在递增后才返回原本的值
要注意在此不需要使用&返回引用,而是返回值即可
因为我们使用的是局部的对象temp,如果返回的是引用,那么函数执行完后该对象被释放,我们获得的引用不存在,这是一个非法操作
所以重载前置++返回引用,重载后置++返回值,这是一个区别
4.4赋值运算符重载
类在创建之后本身会存在一个operator=的重载函数,也就是拷贝构造函数
但这样的拷贝构造函数隐含安全问题
假设有下面这样的代码段
#include <iostream>
using namespace std;
class Person
{
public:
Person(int age)
{
m_Age = new int(age);
}
/*~Person()
{
delete m_Age;
}*/
int *m_Age;
};
int main()
{
Person p1(18);
Person p2(20);
p2 = p1;
cout << "p1:" << *p1.m_Age << ' ' << "p2:" << * p2.m_Age;
return 0;
}
这段程序可以正常运行并输出 p1:18 p2:18
然而我们使用了堆区的内存,在使用后需要手动释放
在加上析构函数后会发现程序报错了
原因是 p2=p1 这样的赋值操作仅仅是将p1中m_Age的值赋给p2的m_Age
也就是说二者指向的是同一块内存,所以后面重复释放导致出错,这也是浅拷贝的一种
具体实现如下:
Person& operator=(Person& p)
{
if (m_Age != NULL)
{
delete m_Age;
m_Age = NULL;
}
m_Age = new int(*p.m_Age); //重新分配内存
return *this; // *this也可以换成 p
}
4.5函数调用运算符重载
函数调用运算符 ()重载后也称为 仿函数
仿函数非常灵活,没有固定的写法
#include <iostream>
using namespace std;
class MyPrint
{
public:
void operator()(string text)
{
cout << text << endl;
}
};
void test1()
{
//重载的()操作符 也称为仿函数
MyPrint myFunc;
myFunc("hello world");
}
class MyAdd
{
public:
int operator()(int v1, int v2)
{
return v1 + v2;
}
};
void test2()
{
MyAdd add;
int ret = add(10, 10);
cout << "ret = " << ret << endl;
//匿名对象调用
cout << "MyAdd()(100,100) = " << MyAdd()(100, 100) << endl;
}
int main()
{
test1();
test2();
return 0;
}
以上笔记想法来自B站黑马程序员教学视频
做作业发现的东西
string头文件中是c++独有的,包含许多内置函数
cstring则是C的东西,包括strcpy()这样方便的函数,但这样的函数在string中却是没有的
string中的函数主要是对两个以上字符串进行处理,较cstring更便捷,如:拼接,插入,删除,提取,查找