一、类与对象定义
1、类的定义
类中包含两部分:①数据成员:描述这一类对象所共同拥有的静态特征数据;②成员函数:这一类对象所共同拥有的特征的行为。
类的定义格式:
class 类名
{
[private:]
私有数据成员和成员函数
protected:
保护数据成员和成员函数
public:
公有数据成员和成员函数
};
其中[]表示可以缺省,为默认属性。通常将成员函数定义为公有,这时函数的原型对外开放;数据成员为私有或保护成员。
成员函数的定义有两种方式
①在类内定义(自动成为内联函数):
函数返回类型 成员函数名(形式参数列表)
{
... //函数体
}
②类内只给出成员函数原型,而在类外实现:
函数返回类型 类名::成员函数名(形式参数表)
{
...//函数体
}
类成员的访问属性及作用
访问属性 | 含义 | 作用 |
private | 私有成员 | 只允许该类的成员函数和友元函数访问,不能被其它函数访问 |
protected | 保护成员 | 既允许该类的成员函数和友元函数访问,也允许其派生类的成员函数访问 |
public | 公有成员 | 既允许该类的成员函数访问,也允许类外部的其它函数访问 |
三种访问修饰符(访问控制符)在类中的出现顺序无严格的限制,甚至可以交叉出现。
私有成员和保护成员的访问属性类似,唯一的区别在于:该类派生新类时,保护成员可被继成和直接访问,而私有成员在派生类中不可直接访问。另外,在C++中,结构体也可以有数据成员和成员函数两大组成部分。与类的区别在于结构体中的所有成员默认访问属性都是public。
2、定义对象
(1)定义
类本身是抽象的,只有定义了对象,系统才会给响应的对象分配存储空间。对象的定义方式通常有两种:
①在定义完类之后定义对象:
类名 对象名1[,对象名2,...,对象名n];
可以同时定义普通的对象、指针、引用、数组等。
②在定义类的同时定义对象
class 类名
{
...//类中的成员
}对象名1[,对象名2,...,对象名n];
(2)访问
对象定义后,有两种访问对象成员的方式:
①圆点访问
对象名.成员 或 (*指向对象的指针).成员
②指针访问
对象指针变量名->成员 或 (&对象名)->成员
3、this指针
系统会为每个对象分配内存空间,这样每个对象都有属于自己的数据。但成员函数代码为该类所有对象共有。而每个成员函数都有一个特殊隐藏的this指针,用来存放当前对象地址。对象调用成员函数时,系统首先将对象的地址赋给this指针,成员函数根据this指针所指位置来提取当前对象的数据成员信息,从而使得不同对象调用同一成员函数所处理的是对象自己的数据成员,不会造成混乱。大部分程序中一般不显式地写出this指针。
二、构造函数与析构函数
类定义对象时,所需的内存空间(对象中成员的初始化)都是由构造函数来完成的,当对象生命结束时,由析构函数完成对象存储空间的回收和相关善后工作。
1、构造函数
类定义对象时,系统会自动调用构造函数来创建并初始化对象。
(1)构造函数的声明和定义
类内:
类名([形参列表])
类外:
类名::类名([形参列表]){...//函数体}
①构造函数名必须与类名相同;
②构造函数没有返回值,前面也不能加void;
③构造函数必须为public属性,否则对象无法自动调用构造函数;
④构造函数是在创建对象时,由系统自动调用,所定义的对象后要提供构造函数所需的实际参数,不能通过对象名.(实际参数表)的方式调用构造函数。
系统调用的构造函数只有在定义无名对象时才可以使用构造函数名(实际参数表)这种形式,如:
Date today;//此时系统调用的是系统默认的构造函数
today = Date(2018, 12, 23); //此处定义无名对象
(2)系统默认构造函数及无惨构造函数的定义
如果定义的类中没有自定义的构造函数,系统会生成一个默认的构造函数,该构造函数无参,也无任何语句,其功能仅用于创建对象,为对象分配空间但不对其中的数据成员初始化,格式如下:
类名::类名(){}
而一旦类中已经提供了一个构造函数,系统就不再会调用默认构造函数(即使在定义对象时不提供实参),在使用构造函数时需要注意:
①一个类可以拥有多个构造函数(重载);
②一旦用户自定义了构造函数,系统不再会为用户提供无参构造函数。
(3)具有默认参数值的构造函数
拥有默认参数值的构造函数可以从一定程度上避免因参数数量不同而找不到合适的构造函数。(同普通函数一样,如果在声明时提供了默认值,那么在定义时不能再提供)
(4)拷贝构造函数
拷贝构造函数也是类的构造函数的一个重载,它能用一个已经存在的对象来初始化另一个新建的对象,且函数中的形式参数是本类对象常引用。C++中为每个类定义了一个默认的拷贝构造函数(实现将实参对象的数据成员复制到新创建的当前对象对应的数据成员中)。
自定义拷贝构造函数格式:
class 类名
{
public:
类名(const 类名 &对象名);
...
};
类名::类名(const 类名 &对象名)
{
...
}
系统自动调用拷贝构造函数的几种情况:
①明确表示由一个对象初始化另一个对象;
②当对象作为(普通)函数的实际参数传递给该函数形式参数,注意,如果形式参数是引用参数或指针参数则不会调用拷贝构造函数,因为此时不会产生新对象;
③当对象作为(普通)函数的返回值时。
注意:拷贝构造函数的形式参数不能为一个该类对象(而应该为引用),因为当将一个对象作为实参传递时会不断调用该拷贝构造函数,造成无线循环。
另外A a(b)等效于A a = b;
2、析构函数
对象生命周期结束时,系统自动调用析构函数释放对象所占内存资源。在类内需要声明public属性的析构函数。格式如下:
~类名();
类外定义格式如下:
类名::~类名()
{
...//函数体
}
①析构函数无返回值,前面也不能加void,必须定义为公有成员函数;
②析构函数没有形式参数,也不能为void,因此不能被重载,每个类只能拥有一个析构函数;
③两种情况下会自动调用析构函数。第一,对象生命周期结束;第二,new运算符创建的对象由delete运算符释放时。
每个类中同样有一个默认的析构函数,通常使用该默认的构造函数就可以了,但如果类中有指针类型的数据成员并且在构造函数中申请了动态内存空间,此时一定要定义析构函数来释放该动态空间。
另外,系统创建对象时调用构造函数的顺序与主函数中创建对象顺序一致,而析构函数的调用顺序与构造函数相反。
三、深拷贝与浅拷贝
如果一个类中包含指向动态存储空间指针类型的数据成员,并且通过该指针在构造函数中申请了子空间,则必须为该类定义一个拷贝构造函数,否则在析构时容易出现意外错误。
浅拷贝:如果在拷贝构造函数中,将对象a内的指针数据成员直接指向b内的指针所指向的空间,此时的拷贝构造函数为浅拷贝,如:
A (const &c)
{
...
a.p = c.p;
}
A(b);
此时对象a和b中的指针p指向了同一段内存空间,如果b被析构了,a中的指针p将成为悬挂指针,其指向内容未知。(默认拷贝构造函数就是浅拷贝)
深拷贝:通过为a中的p重新申请一段内存,并将b中p指向的内容拷贝给a便可实现深拷贝:
A (const &c)
{
...
if(c.p)
{
a.p = new 数据类型[SIZE];
strcmp(a.p, c.p);
}
}
A(b);
四、对象的使用
1、对象数组
定义:
类名 数组名[SIZE];
使用:
数组名[下标].成员
对象数组的初始化:
类名 数组名[SIZE] = {类名(实参列表1, 实参列表2, ... )};
2、对象指针
对象指针的定义方法与普通指针一样。
定义:
类名 *对象指针名;
使用:
指针变量->成员名 或 (*指针变量名) 成员名;
对象数组名实际上就是对象指针常量:
(对象数组名+下标)->成员名 或 *(对象数组名+下标).成员名
3、对象引用
对象引用和变量引用一样,是一个已定义对象的别名,不占用任何内存,必须在定义时初始化:
A a(实参列表);
A &b = a; //这里不产生拷贝,因此不会调用构造函数
引用在编程时最多的用法就是用作函数的形式参数。
4、对象参数
(1)对象作为函数参数
同其它类型作为形参一样,传递实参时将实参对象的内容拷贝给形参,因此会调用拷贝构造函数,而在函数结束时形参对象生命周期结束,自动调用析构函数。
(2)对象指针作为函数参数
实参对象的地址传递给形参时,不会产生新对象,因此不会调用拷贝构造函数,仅形参对象指针占用4个字节(或8个)。可以通过对指针所指向内容的修改达到改变实参对象值的效果。
(3)函数引用作为函数参数
面向对象的程序设计中,多采用对象引用作为形式参数而不采用对象作为值形式参数,因为通过传值方式来传递和返回对象时都会调用拷贝构造函数,会为形式参数对象分配空间,降低了时间和空间效率。而用对象引用作为形式参数时,引用作为实参的别名,不会产生新对象,无需另外分配内存空间,也不会调用拷贝构造函数。
五、友元
C++中通过定义友元来实现使用类的外部函数或另一个类,在不改变类的数据成员安全性的前提下,访问该类中的私有数据成员。
友元的3种形式如下:
①一个不属于任何类的普通函数声明为当前类的友元,称为当前类的友元函数;
②一个其它类的成员函数声明为当前类的友元,称为当前类的友元成员;
③另一个类声明为当前类的友元,称为当前类的友元类;
1、友元函数
将一个普通函数声明为一个类的友元函数,则在该类中声明该函数的原型,同时在前面加上关键friend:
friend 函数返回类型 函数名(形式参数表);
注意:
①友元函数的参数一般为相应的类对象;
②友元函数在类中的声明位置、访问属性不受限制,且如果在类外部定义,此时不需要加关键字friend,也可以在内部定义;
③一个函数访问多个类时,友元函数将会非常有用。
2、友元成员
将类A中的成员函数声明为类B的友元时,必须在A类的定义之前声明B类(向前引用,因为A的成员函数用到了B类),在B类中声明A类的成员函数为B类的友元成员时,格式为:
friend 返回类型 A::函数名(参数表)
如:
class B;
class A
{
...
public:
void FuncA(const B &b);
};
class B
{
...
public:
friend void A::FuncA(const B &b);
void FuncB(...);
};
3、友元类
一个类A可以声明为另一个类B的友元,从而类A中的所有成员函数都是类B的友元成员,可以访问B的所有成员,格式如下:
friend A;
例:
class A;
class B
{
...
public:
friend A;
};
class A
{
...
public:
void FuncA(B &b);
};
①A成员函数具有访问B中所有成员的权限;
②友元关系是单向的,不具交换性,即B类不是A类的友元,B成员函数不能访问A成员;
③友元关系不具传递性,B将A类声明为友元,C将B类声明为友元,A类不是C类的友元。
通过友元机制实现了不同类或对象的成员函数之间、类的成员函数和普通函数之间的数据共享。避免了频繁调用类的接口函数,提高程序的运行速度,从而也提高了程序的运行效率。
注意:无论是哪种形式的友元,虽然都拥有访问类的访问类的所有成员的特权,但它们都不是类的成员。