C++学习笔记(四)——对象特性、友元
写在前面
写本系列的目的(自用)是回顾已经学过的知识、记录新学习的知识或是记录心得理解,方便自己以后快速复习,减少遗忘。以前三天打鱼两天晒网地学习C++,一直无法对C++熟练掌握,核心的面向对象编程并不熟悉,STL语法只会部分,因此也希望自己未来能做到熟悉甚至精通C++。
Part 2 面向对象编程
该部分主要通过b站黑马程序员的视频来进行学习,记录笔记。全部是概念性的东西并不能帮助理解知识,直接上代码并以注释的形式讲解会更易于理解。
二、类和对象
2、对象特性
C++对象特性这部分知识杂且多,不可能马上就能记住并理解。多写代码,在实际操作过程中遇到问题及时复习,才能真正掌握。
(3)拷贝构造函数的调用时机
C++中拷贝构造函数调用时机通常有三种情况
1、使用一个已经创建完毕的对象来初始化一个新对象,这个在上一节也有说明。
2、值传递的方式给函数参数传值。值传递相当于拷贝了一份副本传入函数。
3、以值方式返回局部对象。返回对象时,也是新创建了一个变量,并将值拷贝给它。
#include<iostream>
using namespace std;
class Person
{
private:
int m_Age;
public:
Person()
{
cout<<"默认构造函数"<<endl;
}
Person(int age)
{
m_Age = age;
cout<<"有参构造函数"<<endl;
}
Person(const Person &p)
{
cout<<"拷贝构造函数"<<endl;
m_Age = p.m_Age;
}
~Person()
{
cout<<"析构函数构造"<<endl;
}
};
void test1()
{
Person p1(20); //这一行会调用有参构造函数
Person p2(p1); //使用已经存在的对象初始化一个对象,调用拷贝构造函数
}
void doWork(Person p){ } //值传递相当于拷贝了一份原来p3的临时副本,所以是拷贝构造函数
void test2()
{
Person p3; //这一行会调用默认构造函数
doWork(p3); //值传递的方式给函数参数传值,这一行会调用拷贝构造函数
}
Person doWork2()
{
Person p4; //这一行对调用默认构造函数
return p4; //以返回值的方式传值,相当于创建了一个对象,将p4拷贝过去后返回
} //所以会调用拷贝构造函数
void test3()
{
Person p5 = doWork2();
}
int main()
{
test1(); //结束后调用两次析构函数
cout<<"-----------------"<<endl;
test2(); //结束后调用两次析构函数
cout<<"-----------------"<<endl;
test3(); //结束后调用两次析构函数
cout<<"-----------------"<<endl;
system("pause");
return 0;
}
(4)构造函数的调用规则
默认情况下,c++编译器至少给一个类添加3个函数,用户不写,那么编译器就会给你写。:
默认构造(空实现)、析构函数(空实现)、拷贝构造(值拷贝)。
如果用户定义有参构造函数,c++不在提供默认无参构造,但是会提供默认拷贝构造。这时候假如用户也没用写默认函数,那么可能出现问题。这时候如果你写Person p,采用默认构造,那么就会出错,因为c++编译器也没有给你写默认构造函数,编译器没有默认构造函数调用了。
如果用户定义拷贝构造函数,c++不会再提供其他构造函数。这时候假如用户也没用写另外两个构造函数,那么在写Person p和Person(10)时都会报错,因为编译器没有默认构造函数和有参构造函数调用了。
(5)深拷贝与浅拷贝
浅拷贝:常见的赋值拷贝操作
深拷贝:在堆区重新申请空间,进行拷贝操作
编译器给我们写的拷贝构造函数是浅拷贝,平常并不会出现问题,但在需要申请空间的操作时,配合上析构函数会出现问题。
先补充一下new和delete的用法:在C++中,new和delete是用于动态内存管理的运算符,它们允许在运行时分配和释放内存。用户手动分配和释放的内存都在堆上。
//动态分配单个对象
int* ptr = new int; // 分配一个int大小的内存,值未初始化
*ptr = 42; // 赋值
int* ptr2 = new int(10); // 分配并初始化为10
delete ptr; // 释放内存
delete ptr2;
//动态分配数组
int* arr = new int[5]; // 分配包含5个int的数组,值未初始化
// 初始化数组元素
for (int i = 0; i < 5; ++i) {
arr[i] = i * 2;
}
delete[] arr; // 释放数组内存(必须用[])
需要注意的几个点是:
1、new申请内存后,要记得调用delete清楚,否则可能导致内存泄漏
2、不能对同一块空间重复释放
3、不能释放非new分配的内存
下面来看示例:
#include<iostream>
using namespace std;
class Person
{
private:
int m_Age;
int *m_Height; //指针,指向用户身高所在的地址
public:
Person()
{
cout<<"默认构造函数"<<endl;
}
Person(int age, int height)
{
m_Age = age;
m_Height = new int(height); //这里动态申请了一块int大小的地址,值为用户传入的height
cout<<"有参构造函数"<<endl;
}
~Person()
{
if(m_Height!=nullptr)
delete m_Height; //析构函数的作用于此体现,进行后处理,释放占用空间
cout<<"析构函数"<<endl;
}
};
void test()
{
Person p1(18, 160); //进行有参构造
Person p2(p1); //系统为我们写好了拷贝构造函数,此处是拷贝构造
cout<<"p1的年龄为:"<<p1.m_Age<<"身高:"<<*p1.m_Height<<endl; //输出
cout<<"p2的年龄为:"<<p2.m_Age<<"身高:"<<*p2.m_Height<<endl;
}
int main()
{
test();
system("pause");
return 0;
}
以上代码在运行时系统会出错。出错原因就在拷贝函数身上。系统为我们写的拷贝函数如下:
Person(const Person &p)
{
m_Age = p.m_Age;
m_Height = p.m_Height;
}
利用这个拷贝函数,是将原本p1的内容逐字节拷贝过来,如下图所示:

如图所示,p2的指针和p1的指针中保存的是同一个地址。那么在调用析构函数时,p1和p2会对同一块地址空间执行delete操作。我们知道,不能对同一块空间重复释放,因此系统就报错了。解决方式就是进行深拷贝,我们需要自己书写拷贝构造函数,如下:
Person(const Person &p)
{
m_Age = p.m_Age;
m_Height = new int(*p.m_Height);
}
这样,p2中的指针指向的就是另一块地址空间,只不过这块地址存储的值也是160。
(6)初始化列表
在传统方法中,我们采用构造函数的有参构造函数来为属性赋初值,这里介绍另外一种方法:初始化列表。
初始化列表的语法很简单,构造函数():属性1(值1),属性2(值2)...,如下:
Person():m_A(10),m_B(20),m_C(30)
{
}
显然,这样赋初值的话不够灵活,初值永远不会改变。可以采用下例灵活地赋初值。
#include<iostream>
using namespace std;
class Person
{
public:
int m_A;
int m_B;
int m_C;
Person(int a, int b, int c): m_A(a), m_B(b), m_C(c) //初始化列表初始化属性
{
}
};
void test()
{
Person p(30,20,10);
cout<<"m_A="<<p.m_A<<endl;
cout<<"m_B="<<p.m_B<<endl;
cout<<"m_C="<<p.m_C<<endl;
}
int main()
{
test();
system("pause");
return 0;
}
(7)类对象作为类成员
C++类中的成员可以是另一个类的对象,我们称该成员为 对象成员。
class A{ }
class B
{
A a;
....
}
如上例所示,B类中有对象A作为成员。需要注意的是:
构造顺序为,先调用A的构造函数,再调用B的构造函数。可以理解为先做好部分,才能有整体。
析构顺序为,先调用B的析构函数,再调用A的析构函数。析构与构造的顺序反过来,可以按栈先进后出的顺序来理解。
(8)静态成员
静态成员就是在成员变量和成员函数前加上关键字static。主要有两种静态成员,静态成员变量和静态成员函数。
首先来说静态成员变量,满足三个特点:
1、所有对象共享同一份数据。无论谁修改了静态成员变量,每个成员的静态成员变量都会改变。所以严格来讲,静态成员变量不属于任何一个对象。因此,静态成员变量有两种访问方式,通过对象访问和通过类名访问。
2、在编译阶段分配内存。
3、类内声明,类外初始化。
#include<iostream>
using namespace std;
class Person
{
public:
static int m_A; //静态成员变量,类内声明
private:
static int m_B; //静态成员变量也是有访问权限的
};
int Person::m_A = 10; //类外初始化
int Person::m_B = 10; //注意,虽然是私有的,但是可以初始化。除了初始化的行为都不允许
void test01()
{
//静态成员变量两种访问方式
Person p1; //通过对象
p1.m_A = 100; //注意,这里所有对象的m_A都变为了100
cout << "p1.m_A = " << p1.m_A << endl;
Person p2;
p2.m_A = 200; //注意,这里所有对象的m_A都变为了200
cout << "p1.m_A = " << p1.m_A << endl; //由于共享同一份数据,所以都是200
cout << "p2.m_A = " << p2.m_A << endl;
cout << "m_A = " << Person::m_A << endl; //通过类名
//cout << "m_B = " << Person::m_B << endl; //私有权限访问不到
}
int main() {
test01();
system("pause");
return 0;
}
接下来说静态成员函数,满足以下特点:
1、所有对象共享同一个函数。所以严格来讲,静态成员函数不属于任何一个对象。因此,静态成员函数有两种访问方式,通过对象访问和通过类名访问。
2、静态成员函数只能访问静态成员变量。因为静态成员函数如果访问了非静态成员变量,编译器会不知道它访问的是哪一个成员的非静态成员变量。
#include<iostream>
using namespace std;
class Person
{
public:
static int m_A; //静态成员变量
int m_B; //非静态成员变量
static void func()
{
m_A = 100; //静态成员函数可以访问静态成员变量
//m_B = 200; //这句解开注释会报错,因为静态成员函数不能访问非静态成员变量
cout<<"static void func"<<endl;
}
private:
static void func2() //私有的静态成员函数
{
cout<<"static void func2"<<endl;
}
};
int Person::m_A = 0; //在类外进行初始化
void test()
{
Preson p; //通过对象访问静态成员函数
p.func();
Person::func(); //通过类名访问静态成员函数
//Person::func2(); //如果解开注释会报错,因为类外访问不到私有的静态成员函数
}
int main()
{
test();
system("pause");
return 0;
}
(9)C++对象模型和this指针
C++的成员变量和成员函数是分开存储的。
如果类为空,那么这个类占的空间大小为1个字节;如果类中仅包含一个非静态成员(int类型),那么该类占的空间大小为1个字节。
如果类中仅包含一个非静态成员和一个静态成员变量,类占的空间大小还是4,因为静态成员变量不在类的对象上。如果再加上一个非静态成员函数或是静态成员函数,类占空间大小还是4因为成员变量和成员函数是分开存储的。如下:
class Person {
public:
//非静态成员变量占对象空间
int mA;
//静态成员变量不占对象空间
static int mB;
//函数也不占对象空间,所有函数共享一个函数实例
void func() {
cout << "mA:" << this->mA << endl;
}
//静态成员函数也不占对象空间
static void sfunc() {
}
};
下面来看this指针。
通过以上的示例可以发现,成员函数不属于对象,每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码。这时候,就需要用this指针区分是哪个对象调用的自己。例如,现在有两个对象p1,p2。p1调用成员函数PersonAddAge(),那么此时this指针就会指向p1。即this指针指向的是被调用的成员函数所属的对象。
this指针是隐含每一个非静态成员函数内的一种指针,不需要定义,直接使用即可。上述操作都是编译器隐式实现的,我们不需要使用。
如果需要使用this指针,它的作用有以下两个:
1、当形参和成员变量同名时,可用this指针来区分。这个在下面的代码中的参数构造函数中得以体现。
2、在类的非静态成员函数中返回对象本身,可使用return *this,这个在下面代码中的成员函数中得以体现。
#include<iostream>
using namespace std;
class Person
{
public:
int age;
Person(int age)
{
this->age = age; //这里,形参age和成员变量age同名,如果写成age = age就会
} //出错,因此需要用this指针,当然也可以修改形参名称
Person& PersonAddAge(Person p) //这里返回的值是对象本身,所以是Person&
{ //!!!注意,如果这里使用的是Person,返回的值就不是p2
this->age += age; //会是编译器为你重新创建的对象
return *this;
}
};
void test1()
{
Person p1(10);
Person p2(10);
p2.PersonAddAge(p1).PersonAddAge(p1).PersonAddAge(p1); //这是一种链式编程思想
//其实和cout的思想差不多,每次调用完返回
cout<<"p2的年龄为"<<p2.age<<endl; //p2本身,这样就可以继续往下调用
}
int main()
{
test1();
system("pause");
return 0;
}
(10)空指针访问成员函数
c++中空指针也可以访问成员函数。只不过要注意此时有没有用到this指针。见下例
#include<iostream>
using namespace std;
class Person {
public:
int m_Age;
void ShowClassName()
{
cout << "我是Person类!" << endl;
}
void ShowPerson()
{
cout << m_Age << endl;
}
};
void test1()
{
Person * p = NULL;
p->ShowClassName(); //空指针,可以调用成员函数
//p->ShowPerson(); //解注释会报错,因为成员函数中用到了this指针
} //相当于此时的ShowPerson函数中,系统默认使用this->m_Age。但是p是
//空指针,类似于你没有女朋友,何谈女朋友的年龄(?)
int main() {
test01();
system("pause");
return 0;
}
(11)const修饰成员函数
常函数:成员函数后加const后我们称为这个函数为常函数。常函数内不可以修改成员属性,但成员属性声明时加关键字mutable后,在常函数中依然可以修改。
在这里说一下this指针的本质,this指针的本质是指针常量,当你调用一个对象的成员函数时,指针的指向不能修改。它相当于Person *const this。const修饰成员函数后,成员函数内就不能再修改成员变量了,也就是this指针指向的值也不能修改了,此时相当于const Person *const this。语法为在成员函数后面加const。
常对象:声明对象前加const称该对象为常对象,常对象只能调用常函数,常对象不可以修改成员属性,但成员属性声明时加关键字mutable后,常对象就可以修改了。
#include<iostream>
using namespace std;
class Person
{
public:
int m_A;
mutable int m_B;
void showPerson() const //这是一个常函数
{
//this->m_A = 100; //这句代码解注释会报错,因为常函数不能修改指针指向的值
this->m_B = 100; //如果仍然想修改,就在成员属性前加mutable
}
void func()
{
}
};
void test1()
{
Person p;
p.showPerson();
}
void test2()
{
const Person p; //这是一个常对象
//p.m_A = 100; //这句代码解注释会报错,因为常对象不能修改普通成员变量的值
p.m_B = 100; //如果仍然想修改,就在成员属性前加mutable
//常对象只能调用常函数
//p.func(); //这句代码解注释会报错,因为常对象不能调用普通函数
} //因为普通函数可以修改属性,而常对象是不允许的,二者相悖
int main()
{
test1();
test2();
system("pause");
return 0;
}
3、友元
生活中你的家有客厅(Public),有你的卧室(Private)。
客厅所有来的客人都可以进去,但是你的卧室是私有的,也就是说只有你能进去。但是呢,你也可以允许你的好闺蜜好基友进去。
我们已经知道,类的私有属性是不能在类外访问的,但有些私有属性也想让类外特殊的一些函数或者类进行访问,就需要用到友元的技术。友元的目的就是让一个函数或者类访问另一个类中私有成员。
友元的关键字是friend。有三种实现方式:
1、全局函数做友元
2、类做友元
3、成员函数做友元
(1)全局函数做友元
全局函数做友元,就是让全局函数能够访问类的私有属性。实现方式只需要把全局函数的声明放到类的开头。并且在声明前加上friend。正如示例中的friend void goodGay(Building *building);一样。
#include<iostream>
#include<string>
using namespace std;
class Building
{
friend void goodGay(Building *building); //告诉编译器 goodGay全局函数 是 Building类的
//好朋友,可以访问类中的私有内容
public:
string m_SittingRoom;
private:
string m_BedRoom;
public:
Building()
{
m_SittingRoom = "客厅";
m_BedRoom = "卧室";
}
};
void goodGay(Building *building)
{
cout<<"好基友全局函数 正在访问 :"<< building->m_SittingRoom<<endl;
cout<<"好基友全局函数 正在访问 :"<< building->m_BedRoom<<endl; //是友元,所以可以访问
}
int main()
{
Building b;
goodGay(&b);
system("pause");
return 0;
}
(2)类做友元
类做友元就是让类能访问另一个类的私有属性。实现方式只需要把类的声明放到类的开头。并且在声明前加上friend。正如示例中的friend class GoodGay;一样。
#include<iostream>
#include<string>
using namespace std;
class Building
{
friend class GoodGay;
public:
string m_SittingRoom; //客厅
private:
string m_BedRoom; //卧室
public:
Building(); //这里是类外实现成员函数,所以类内只用写函数声明
};
class GoodGay
{
public:
GoodGay();
void visit();
Building *building;
}
//类外实现成员函数
Building::Building()
{
m_SittingRoom = "客厅";
m_BedRoom = "卧室";
}
GoodGay::GoodGay()
{
building = new Building;
}
void GoodGay::visit()
{
cout<<"好基友函数 正在访问 :"<< building->m_SittingRoom<<endl;
cout<<"好基友函数 正在访问 :"<< building->m_BedRoom<<endl; //是友元,所以可以访问
}
int main()
{
GoodGay gg;
gg.visit();
system("pause");
return 0;
}
(3)成员函数做友元
成员函数做友元就是让成员函数能访问另一个类的私有属性。实现方式只需要把成员函数的声明放到类的开头,并且要标注该函数所属的作用域/所属类,在这个案例里就是加上GoodGay::。并且在声明前加上friend。正如示例中的friend void GoodGay::visit();一样。
#include<iostream>
#include<string>
using namespace std;
class Building
{
friend void GoodGay::visit();
public:
string m_SittingRoom; //客厅
private:
string m_BedRoom; //卧室
public:
Building(); //这里是类外实现成员函数,所以类内只用写函数声明
};
class GoodGay
{
public:
GoodGay();
void visit(); //让该函数可以访问Building中的私有成员
void visit2(); //让该函数不可以访问Building中的私有成员
Building *building;
}
//类外实现成员函数
Building::Building()
{
m_SittingRoom = "客厅";
m_BedRoom = "卧室";
}
GoodGay::GoodGay()
{
building = new Building;
}
void GoodGay::visit()
{
cout<<"visit函数 正在访问 :"<< building->m_SittingRoom<<endl;
cout<<"visit函数 正在访问 :"<< building->m_BedRoom<<endl; //是友元,所以可以访问
}
void GoodGay::visit2()
{
cout<<"visit2函数 正在访问 :"<< building->m_SittingRoom<<endl;
//cout<<"visite2函数 正在访问 :"<< building->m_BedRoom<<endl; //不是友元,不能访问
}
int main()
{
GoodGay gg;
gg.visit();
gg.visit2();
system("pause");
return 0;
}
更多推荐
所有评论(0)