四、动态内存分配的相关问题
1、为对象分配动态存储空间
(1)到目前为止,在为程序中定义的对象分配内存空间时采用的都是“静态存储方式”,在编译时就确定了所占存储空间的大小,而与之相对的动态存储分配技术则可以保证在程序运行过程中按照实际需要申请适量内存,使用结束后再进行释放,这种在程序运行过程中根据需要可以随时建立或删除的对象称为自由存储对象,建立和删除工作分别由运算符new和delete完成。
(2)用new创建单个对象时,要根据参数调用相应的构造函数;在用new创建对象数组时,会调用默认构造函数;用delete删除对象时,要调用析构函数。
(3)举例:
#include<iostream>
using namespace std;
class Person
{
public:
Person(int _age, int _height, bool _sex = false) : age(_age), sex(_sex)
{
cout << "Person构造函数的调用" << endl;
height = _height;
}
Person()
{
cout << "Person构造函数的调用" << endl;
}
~Person()
{
cout << "~Person析构函数的调用" << endl;
}
int age;
int height;
bool sex;
};
int main() {
Person *point;
point= new Person(18, 160, true);
//调用一次对象p的构造函数
delete point;
//调用一次对象p的析构函数
point = new Person[2];
//调用两次对象p的构造函数
delete[] point;
//调用两次对象p的析构函数
return 0;
}
2、深拷贝与浅拷贝
(1)浅拷贝是简单的赋值拷贝操作,而深拷贝是在堆区重新为成员变量申请空间,进行拷贝操作,如果成员属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题。
(2)举例:
#include<iostream>
using namespace std;
class Person
{
public:
//无参(默认)构造函数
Person()
{
cout << "无参构造函数!" << endl;
}
//有参构造函数
Person(int age, int height)
{
cout << "有参构造函数!" << endl;
m_age = age;
m_height = new int(height);
}
//拷贝构造函数
Person(const Person& p)
{
cout << "拷贝构造函数!" << endl;
//如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放堆区问题
m_age = p.m_age;
m_height = new int(*p.m_height);
}
//析构函数
~Person()
{
cout << "析构函数!" << endl;
if (m_height != NULL)
{
delete m_height;
m_height = NULL;
}
}
public:
int m_age;
int* m_height;
};
void test01()
{
Person p1(18, 180);
Person p2(p1);
cout << "p1的年龄: " << p1.m_age << " 身高: " << *p1.m_height << endl;
cout << "p2的年龄: " << p2.m_age << " 身高: " << *p2.m_height << endl;
}
int main() {
test01();
system("pause");
return 0;
}
五、静态成员
1、概述
(1)对于类中的非静态数据成员,每一个类对象都拥有一个副本,即每个对象的同名数据成员可以分别存储不同的数值,而类中的静态成员则不是,甭管一个类创建了多少个对象,这个静态成员都只有一个副本,这个副本由所有属于这个类的对象共享。
(2)静态成员表示整个类范围的信息,其声明以static关键字开始,包括静态数据成员和静态成员函数。
2、静态数据成员
(1)静态数据成员在每个类对象中并不占有存储空间,它只是在每个类中分配有存储空间供所有对象公用,内存分配在编译阶段就已经完成。
(2)在对静态数据成员初始化时应注意:
①在类中使用关键字static对静态数据成员进行声明,在类体外对静态数据成员进行初始化(静态数据成员的初始化与它的访问控制权限无关)。
②静态数据成员初始化时前面不加static关键字,以免与一般静态变量或对象混淆。
③由于静态数据成员是类的成员,因此在初始化时必须使用作用域运算符(::)限定它所属的类。
(3)举例:
#include<iostream>
using namespace std;
#include<string>
class Person
{
public:
string m_name;
static int m_A; //所有对象都共享同一份数据;在编译阶段就分配内存;类内声明,类外初始化
private:
static int m_B;
};
int Person:: m_A = 100;
int Person:: m_B = 200;
void test01()
{
Person p;
cout << p.m_A << endl;
Person p2;
p.m_A = 200;
cout << p.m_A << endl;
//cout << p.m_B << endl; //静态成员变量也是有访问权限的
};
void test02()
{
//静态成员变量不仅属于某个对象,所有对象共享同一份数据,所以静态成员变量有两种访问方式
//通过对象进行访问
Person p;
cout << p.m_A << endl;
//通过类名进行访问
cout << Person::m_A << endl;
}
int main() {
test01();
test02();
system("pause");
return 0;
}
3、静态成员函数
(1)公有的静态数据成员可以直接访问,但私有的或保护的静态数据成员必须通过公有的接口进行访问,一般将这个公有的接口定义为静态成员函数。
(2)使用static关键字声明的成员函数就是静态成员函数,静态成员函数也属于整个类而不属于类中的某个对象,它是该类的所有对象共享的成员函数。
(3)静态成员函数可以在类体内定义,也可以在类外定义,当在类外定义时,要注意不能使用static关键字作为前缀。
(4)静态成员函数可以直接访问类中说明的静态成员,但不能直接访问类中说明的非静态成员,若要访问非静态成员,必须通过参数传递的方式得到相应的对象,再通过对象进行访问。
(5)公有的静态成员既可以直接使用作用域运算符通过类名进行访问,也可以通过类的任何对象进行访问,建议使用前者进行访问,即:
<类名>::<静态数据成员名> 或 <类名>::<静态成员函数名>(<参数表>)
#include<iostream>
using namespace std;
class Person
{
public:
static void func() //静态成员函数只能访问静态成员变量;所有对象共享同一个函数
{
cout << "static void func()调用" << endl;
m_A = 100;
//a = 100; 不能访问非静态成员变量,它无法区分这是哪个对象的成员
func2(); //类内可以访问私有权限的静态成员函数
}
static int m_A; //静态成员变量
int a;
private:
static void func2()
{
cout << "static void func2()调用" << endl;
m_A = 300;
}
};
int Person::m_A = 0;
void test01()
{
//通过对象访问函数
Person p;
p.func();
//通过类名访问函数
Person::func();
//Person::func2(); 类外不能访问私有权限的静态成员函数
};
int main() {
test01();
system("pause");
return 0;
}
六、this指针
1、成员变量和成员函数的存储
在C++中,类内的成员变量和成员函数分开存储,只有非静态成员变量才存储在类的对象上。
#include<iostream>
using namespace std;
class Person1
{
public:
};
class Person2
{
public:
int m_A; //非静态成员变量,存储在类的对象上
};
class Person3
{
public:
static int m_B; //静态成员变量,不属于类的对象上
};
int Person3::m_B = 10;
class Person4
{
public:
void func() //非静态成员函数,不属于类的对象上
{
}
};
class Person5
{
public:
static void func() //静态成员函数,不属于类的对象上
{
}
};
void test01()
{
Person1 p1;
cout << "size of p1 =" << sizeof(p1) << endl;
//空对象占用内存空间为1,因为C++编译器会给每个空对象分配一个字节空间,以区分空对象占内存的位置
//每个空对象也都应该有一个独一无二的内存地址
Person2 p2;
cout << "size of p2 =" << sizeof(p2) << endl; //占用内存空间为4
Person3 p3;
cout << "size of p3 =" << sizeof(p3) << endl; //占用内存空间为1
Person4 p4;
cout << "size of p4 =" << sizeof(p4) << endl; //占用内存空间为1
Person5 p5;
cout << "size of p5 =" << sizeof(p5) << endl; //占用内存空间为1
};
int main() {
test01();
system("pause");
return 0;
}
2、this指针详解
(1)每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码,为了让非静态成员函数知晓自己被哪个对象调用,C++提供了一个特殊的对象指针——this指针,它是成员函数所属对象的指针,指向类对象的地址,成员函数可以通过这个指针知道自己属于哪一个对象。
(2)this指针是一个隐含的指针,它隐含于每个类的非静态成员函数中,明确地表示出了成员函数当前操作的数据所属的对象。
(3)this指针不需要定义,直接使用即可。
(4)this指针的用途:
①当形参和成员变量同名时,可用this指针来区分。(当形参和成员变量同名时,不使用this指针会将同名标识符默认为形参;没有同名情况可以不使用this指针)
②在类的非静态成员函数中返回对象本身,可使用return *this。
#include<iostream>
using namespace std;
class Person
{
public:
Person(int age)
{
this->age = age; //this指针指向被调用的成员函数所属的对象
}
int age;
void PersonAddAge(Person &p)
{
this->age += p.age;
}
Person& PersonAddAge2(Person &p) //不要以值的方式返回,这样每调用一次函数就会创建一个新对象,在链式编程中容易出现错误
{
this->age += p.age;
return *this;
}
};
void test01()
{
Person p1(18); //(构造函数中)this指针指向p1
cout << "p1的年龄为" << p1.age << endl;
Person p2(20);
p2.PersonAddAge(p1); //this指针指向p2
cout << "p2的年龄为" << p2.age << endl;
p2.PersonAddAge2(p1).PersonAddAge2(p1).PersonAddAge2(p1); //链式编程思想
//this是指向p2的指针,而*this就是p2这个对象本体
cout << "p2的年龄为" << p2.age << endl;
};
int main() {
test01();
system("pause");
return 0;
}
3、空指针访问成员函数
C++中空指针也是可以调用成员函数的,但是也要注意有没有用到this指针,如果用到this指针,需要加以判断保证代码的健壮性。
#include<iostream>
using namespace std;
//空指针调用成员函数
class Person
{
public:
int m_Age;
void shouClassName()
{
cout << "this is Person class" << endl;
}
void showClassAge()
{
if (this == NULL)
{
return; //如果指针为空,赶紧返回,防止程序崩溃
}
cout << "age = " << m_Age << endl; //相当于cout << "age = " << this->m_Age << endl;
}
};
void test01()
{
Person * p= NULL;
p->shouClassName();
p->showClassAge(); //传进去的指针是NULL,this不知道指向谁
};
int main() {
test01();
system("pause");
return 0;
}
七、常成员
1、常对象
(1)使用const关键字修饰的对象称为常对象,它的定义格式如下:
<类名> const <对象名> 或 const <类名> <对象名>
(2)常对象在定义时必须进行初始化,且不能被更新。(成员属性声明时加关键字mutable后,在常函数中依然可以修改)
(3)常对象只能调用常函数。
2、常成员函数
(1)使用const关键字说明的成员函数称为常成员函数,它的说明格式如下:
<返回类型> <成员函数名>(<参数表>) const;
(2)常成员函数不能更新对象的数据成员,否则会产生错误。
(3)const关键字可以用于参与对重载函数的区分,重载的原则是:当类中有两个或多个同名的一般成员函数和常成员函数时,常对象调用常成员函数,一般对象调用一般成员函数;当类中只有一个常成员函数时,一般对象也可以调用该常成员函数。
3、常数据成员
使用const说明的数据成员称为常数据成员,常数据成员同样也必须进行初始化,并且不能被更新,且常数据成员的初始化只能通过构造函数的成员初始化列表显式进行。
4、举例
#include<iostream>
using namespace std;
class Person
{
public:
int m_A;
mutable int m_B; //特殊变量,即使在常函数中依然可以修改
const int m_C;
void showPerson() const
{
//this->m_A = 100; 在成员函数后加const就是修饰this指针,相当于const Person * const this;
//this = NULL; this指针的本质是指针常量,指向不可修改,有没有const都一样
this->m_B = 100;
}
void func()
{
}
Person(int C) : m_C(C) //常数据成员的初始化只能通过构造函数的成员初始化列表显式进行
{
}
};
void test01()
{
Person p(25);
p.showPerson();
};
void test02()
{
const Person p(20); //常对象
//p.m_A = 100; 常对象的属性不可以乱改
p.m_B = 100; //m_B是特殊变量,在常对象下也可以修改
p.showPerson();
//p.func(); 常对象只能调用常函数,不能调用普通成员函数,否则可能会造成普通属性的修改
};
int main() {
test01();
test02();
system("pause");
return 0;
}
八、友元
1、概述
(1)类具有数据封装和隐藏的特性,只有类的成员函数才能访问类的私有成员和保护成员,外部函数只能访问类的公有成员,但在某些情况下又需要在类的外部访问类的私有成员和保护成员,为了解决这个问题,C++引入了友元,友元可以在类外部直接访问类的私有成员和保护成员。
(2)对于一个类,可以利用friend关键字将一般函数、其它类的成员函数或者是其它类声明为该类的友元,使得这个类中本来隐藏的信息(包括私有成员和保护成员)可以被友元所访问,如果友元是一般成员函数或者是类的成员函数则称为友元函数,如果友元是一个类则称为友元类,友元类的所有成员函数都成为友元函数。
2、友元函数
(1)友元函数不是当前类的成员函数,而是独立于当前类的外部函数(包括普通函数和其它类的成员函数),但它可以访问该类的所有成员,包括私有成员、保护成员和公有成员。
(2)友元函数要在类定义时声明,声明时要在其函数名前加上关键字friend,该声明可以放在公有部分,也可以放在私有部分或是保护部分。友元函数的定义通常在类外部。
(3)两种友元函数:
①全局函数做友元:
#include<iostream>
using namespace std;
#include<string>
class Building
{
friend void goodGay(Building &building); //goodGay全局函数是Building的好朋友,可以访问Building中的私有成员
public:
string m_SittingRoom; //客厅
Building()
{
m_SittingRoom = "客厅";
m_BedRoom = "卧室";
}
private:
string m_BedRoom; //卧室
};
void goodGay(Building &building)
{
cout << "好朋友全局函数 正在访问:" << building.m_SittingRoom << endl;
cout << "好朋友全局函数 正在访问:" << building.m_BedRoom << endl;
};
void test01()
{
Building building;
goodGay(building);
};
int main() {
test01();
system("pause");
return 0;
}
②其它类的成员函数做友元:
#include<iostream>
using namespace std;
#include<string>
class Building; //先让GoodGay中的visit函数被声明,否则到friend一行编译器会不认识visit函数
class GoodGay
{
public:
GoodGay();
Building * building;
void visit(); //希望visit可以访问Building中的私有成员
void visit2(); //希望visit2不可以访问Building中的私有成员
};
class Building
{
friend void GoodGay::visit(); //告诉编译器,GoodGay类下的visit成员函数作为本类的好朋友可以访问本类的私有内容
public:
Building();
string m_SittingRoom; //客厅
private:
string m_BedRoom; //卧室
};
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 << "visit2函数正在访问:" << building->m_BedRoom << endl;
}
void test01()
{
GoodGay gg;
gg.visit();
gg.visit2();
};
int main() {
test01();
system("pause");
return 0;
}
3、友元类
(1)友元除了可以是函数外,还可以是类,即一个类可以作为另一个类的友元,称为友元类,友元类的所有成员函数都是友元函数。
(2)友元类要在类定义时声明,声明时要在类名名前加上关键字friend,该声明可以放在公有部分,也可以放在私有部分或是保护部分。
(3)举例:
#include<iostream>
using namespace std;
#include<string>
class Building; //声明一下后面有写这个类,防止报错
class GoodGay
{
public:
Building * building;
void visit(); //参观函数,访问Building中的属性
GoodGay();
};
class Building
{
friend class GoodGay; //GoodGay类是本类的好朋友,可以访问本类的私有成员
public:
string m_SittingRoom; //客厅
Building();
private:
string m_BedRoom; //卧室
};
//类外写成员函数
Building::Building()
{
m_SittingRoom = "客厅";
m_BedRoom = "卧室";
}
GoodGay::GoodGay()
{
//在堆区创建建筑物对象
building = new Building;
}
void GoodGay::visit()
{
cout << "好朋友类正在访问:" << building->m_SittingRoom << endl;
cout << "好朋友类正在访问:" << building->m_BedRoom << endl;
}
void test01()
{
GoodGay gg;
gg.visit();
};
int main() {
test01();
system("pause");
return 0;
}
九、对象数组
1、对象数组的概念
对象数组是指数组元素为对象的数组,该数组中的每一个元素都是同一个类的对象。
2、对象数组的定义
<类名> <数组名>[<大小>];
3、访问对象数组元素的成员的形式
<数组名>[<下标>].<成员名> 或 (<数组名>+<下标>)-><成员名>
十、涉及类的分文件编写
所有代码挤在一个文件中,代码量过大,可以将部分代码移至分文件中,如下例所示: