您现在的位置是:首页 >技术教程 >【C++ 程序设计】第 3 章:类和对象进阶网站首页技术教程
【C++ 程序设计】第 3 章:类和对象进阶
目录
一、构造函数
(1)构造函数的作用
对于 C++ 中基本数据类型的变量,可以声明全局变量和函数内部的局部变量
变量 初始化 全局变量 如果程序员在声明变量时没有进行初始化,则系统自动为其初始化为 0 。
这个工作在程序启动时完成局部变量 系统不进行自动初始化,所以它的初值需要靠程序员给定。
如果程序员没有设定,则是一个随机值。
- 为了对对象进行初始化,C++ 提供了一种称为构造函数的机制,用于对对象进行初始化,实际上是用来为成员变量赋初值的。
- 构造函数是类中的特殊成员函数,它属于类的一部分。给出类定义时,由程序员编写构造函数。如果程序员没有编写类的任何构造函数,则由系统自动添加一个不带参数的构造函数。
- 声明对象后,可以使用 new 运算符为对象进行初始化,此时调用的是对象所属类的构造函数。构造函数的作用是完成对象的初始化工作,用来保证对象的初始状态是确定的。在对象生成时,系统自动调用构造函数,用户在程序中不会直接调用构造函数。
(2)构造函数的定义
① 定义
- 定义一个类时,需要为类定义相应的构造函数
- 构造函数的函数名与类名相同,没有返回值
- 一个类的构造函数可以有多个,即构造函数允许重载
- 同一个类的多个构造函数的参数表一定不能完全相同
② 声明格式
【声明格式】构造函数的声明格式如下:
- 类名(形参1, 形参2, …,形参n);
【说明】⚫ 在声明类的构造函数时可以同时给出函数体,这样的构造函数称为内联函数。
- 也可以在类体外给出构造函数的定义。
- 构造函数的声明中,形参的个数可以为 0 ,即参数表为空。
⚫ 当类中没有定义任何构造函数时,系统会自动添加一个参数表为空、函数体也为空的构造函数,称为 默认构造函数 。
- 所以任何类都可以保证至少有一个构造函数。
⚫ 如果程序员在程序中已经定义了构造函数,则系统不会再添加默认构造函数。
③ 在类体外定义构造函数的 3 种形式
假设类的成员变量是 x1,x2,…,xn ,则在类体外定义构造函数时通常有如下 3 种形式:【形式一】
类名::类名(形参1,形参2,…,形参n):x1(形参1), x2(形参2), …, xn(形参n){}
【形式二】
类名::类名(形参1,形参2,…,形参n){x1=形参1;x2=形参2;……xn=形参n;}【形式三】
类名::类名( ){x1=初始化表达式1;x2=初始化表达式2;……xn=初始化表达式n;}
/* 示例:类 myDate 定义了两个构造函数 */
myDate::myDate( )
{
year = 1970;
month = 1;
day = 1;
}
myDate::myDate(int y, int m, int d)
{
year = y;
month = m;
day = d;
}
/* 示例:构造函数的定义 */
// 写法 1:使用固定值在初始化列表中为各成员变量赋初值
myDate::myDate():year(1970),month(1),day(1){}
// 写法 2:使用带入的参数值通过初始化列表为各成员变量赋初值
myDate::myDate(int y, int m, int d):year(y), month(m), day(d){}
/* 示例:构造函数也可以在类体外定义 */
// 假设:类 myDate 中已经声明了下列 4 个构造函数:
myDate( ); //不带参数
myDate(int); //带 1 个参数
myDate(int, int); //带 2 个参数
myDate(int, int, int); //带 3 个参数
// 则:类体外,构造函数的定义如下所示:
myDate::myDate():year(1970),month(1),day(25){} //不带参数
myDate::myDate(int d):year(1970),month(1) //带 1 个参数
{
day = d;
}
myDate::myDate(int m, int d):year(1970) //带 2 个参数
{
month = m;
day = d;
}
myDate::myDate(int y, int m, int d) //带 3 个参数
{
year = y;
month = m;
day = d;
}
(3)构造函数的使用
- C++ 语言规定,创建类的任何对象时都一定会调用构造函数进行初始化
- 对象需要占据内存空间,生成对象时,为对象分配的这段内存空间的初始化由构造函数完成
- 定义对象时,类的构造函数会被自动调用
- 特别地,如果程序中声明了对象数组,即数组的每个元素都是一个对象,则一定要为对象所属的这个类定义一个无参的构造函数。
- 因为数组中每个元素都需要调用无参的构造函数进行初始化,所以必须要有一个不带参数的构造函数
/* 示例:使用构造函数的默认参数创建对象 */
// 假设: 类 myDate 中仅定义了如下的构造函数:
myDate::myDate(int y =1970,int m =2,int d =14) // 3 个参数均有默认值
{
year = y;
month = m;
day = d;
}
// 则:创建对象时,可以使用下列形式:
myDate d0;
myDate d1(1980);
myDate d2(1990,3);
myDate d3(2000,4,18);
// 执行结果:输出这 4 个对象的值:
1970/2/14
1980/2/14
1990/3/14
2000/4/18
- 如果程序中声明了对象数组,即数组的每个元素都是一个对象,则一定要为对象所属的这个类定义一个无参的构造函数。
- 因为数组中每个元素都需要调用无参的构造函数进行初始化,所以必须要有一个不带参数的构造函数。
在构造函数中,需要为类中声明的所有的成员变量赋初值。 对于基本数据类型的成员变量,如果程序中没有进行显式的初始化,则系统使用 0 进行初始化。
/* 示例: 构造函数的使用 */
// [普例] 声明了对象数组 A: 此时系统要调用无参的构造函数,为数组 A 的 3 个元素进行初始化,即:
myDate A[3];
// [特例] 声明数组 A 时同时给各元素赋了初值:
myDate A[3]={myDate(1), myDate(10,25), myDate(1980,9,10)};
(4)复制构造函数与类型转换构造函数
① 定义
- 复制构造函数是构造函数的一种,也称为拷贝构造函数
- 它的作用是使用一个已存在的对象去初始化另一个正在创建的对象
- 例如,类对象间的赋值是由复制构造函数实现的
- 复制构造函数只有一个参数,参数类型是本类的引用
- 复制构造函数的参数可以是 const 引用,也可以是非 const 引用
- 一个类中可以写两个复制构造函数,一个函数的参数是 const 引用,另一个函数的参数是非const 引用
- 这样,当调用复制构造函数时,既能以常量对象(初始化后值不能改变的对象)作为参数,也能以非常量对象作为参数去初始化其他对象
② 格式
对于类 A 而言,复制构造函数的原型如下:【格式一】
- A::A(const A&)
【格式二】
- A::A(A &)
/* 示例:复制构造函数与类型转换构造函数 */
// 假设:有定义的类 myDate 和类 Student ,则主函数中可以有如下的语句:
Student stud;
Student ss[2] = {stud, Student()};
// 第 2 条语句也可以写为如下共 3 条语句:
Student ss[2];
ss[0] = Student(stud);
ss[1] = Student();
// 创建数组 ss 时,具体来说,是创建 ss[0] 中的对象时,用到了默认复制构造函数
/* 示例:可以在类中定义自己的复制构造函数 */
// 假设:自定义复制构造函数在类 Student 中声明复制构造函数的原型:
Student(const Student &s);
// 复制构造函数的函数体如下:
Student::Student(const Student &s)
{
name = s.name;
birthday = s.birthday;
}
// 则:在主函数中调用自定义的复制构造函数的语句如下:
Student ss[2] = {stud, Student()};
stud.printStudent();
③ 自动调用复制构造函数的 3 种情况
【情况一】
- 当用一个对象去初始化本类的另一个对象时,会调用复制构造函数。例如,使用下列形式的说明语句时,即会调用复制构造函数:
类名 对象名2(对象名1); 类名 对象名2=对象名1;
【情况二】
- 如果函数 F 的参数是类 A 的对象,那么当调用 F 时,会调用类 A 的复制构造函数。
- 换句话说,作为形参的对象,是用复制构造函数初始化的,而且调用复制构造函数时的参数,就是调用函数时所给的实参。
【情况三】
- 如果函数的返回值是类A的对象,那么当函数返回时,会调用类A的复制构造函数。
- 也就是说,作为函数返回值的对象是用复制构造函数初始化的,而调用复制构造函数时的实参,就是 retrun 语句所返回的对象。
- 注意,在复制构造函数的参数表中,加上 const 是更好的做法。这样复制构造函数才能接收常量对象作为参数,即才能以常量对象作为参数去初始化别的对象。
二、析构函数
(1)概念
⚫ 与构造函数一样,析构函数也是成员函数的一种,它的名字也与类名相同,但要在类名前面加一个 “ 〜 ” 字符,以区别于构造函数。
- 析构函数没有参数,也没有返回值
- 一个类中有且仅有一个析构函数,如果程序中没有定义析构函数,则编译器自动生成默认的析构函数
- 析构函数不可以多于一个,不会有重载的析构函数
- 默认析构函数的函数体为空
⚫ 创建对象时自动调用构造函数, 在对象消亡时自动调用析构函数。
- 析构函数的作用是做一些善后处理的工作
- 例如,如果在创建对象时使用 new 运算符动态分配了内存空间,则在析构函数中应该使用delete 释放掉这部分占用的空间,保证空间可再利用
⚫ 当使用 new 运算符生成对象指针时,自动调用本类的构造函数。
- 使用 delete 删除这个对象时,首先为这个动态对象调用本类的析构函数,然后再释放这个动态对象占用的内存
⚫ 对于对象数组,要为它的每个元素调用一次构造函数和析构函数。
- 全局对象数组的析构函数在程序结束之前被调用
(2)示例
/* 示例:使用 delete 语句析构函数 */
// 假设: 现在修改类myDate和类Student的无参构造函数和析构函数如下:
myDate::myDate(): year(1970), month(1), day(10)
{
cout<<"myDate 构造函数"<<endl;
}
myDate::~myDate()
{
cout<<"myDate 析构函数"<<endl;
}
Student::Student() : name("Noname"), birthday(myDate())
{
cout<<"Student 构造函数"<<endl;
}
Student: :~Student()
{
cout<<"Student 析构函数"<<endl;
}
// 如果:主函数中执行以下语句:
Student *stud = new Student();
delete stud;
// 执行结果:则得到的信息如下:
myDate构造函数
Student构造函数
Student析构函数
myDate析构函数
/* 示例:对于对象数组,要为它的每个元素调用一次构造函数和析构函数。全局对象数组的析构函数在程序结束之前被调用 */
// 假设: 对象数组与 delete 语句如下:
Student *ss = new Student[2];
delete [ ]ss;
// 表达式 new Student[2] 首先分配 2 个 Student 类的对象所需的内存,然后为这 2 个对象各调用一次构造函数。
// 当使用 delete 释放动态对象数组时,通过 “[ ]” 告诉编译器 ss 是对象数组,所以也为这 2 个对象各调用一次析构函数。
// 执行结果:执行这两行语句得到的显示信息如下:
my Date构造函数
Student构造函数
myDate构造函数
Student构造函数
Student析构函数
my Date析构函数
Student析构函数
myDate析构函数
/* 示例:下面的语句创建了对象指针数组,消亡时,要分别释放空间 */
// 析构函数的调用执行顺序与构造函数刚好相反:
Student *ss[2] = {new Student(), new Student()};
delete ss[0];
delete ss[1];
以下代码主要演示了 C++ 动态内存分配、类的定义与调用、成员变量的存取控制等内容:
/* 示例:动态内存分配、类的定义与调用、成员变量的存取控制 */ #include <iostream> // 引入头文件 iostream(输入输出流库) using namespace std; // 使用命名空间 std class Samp // 定义一个名为 Samp 的类 { public: // 访问权限 public void Setij(int a, int b) // 成员函数 Setij ,用于设置成员变量 i 和 j 的值 { i = a; // 将传入值 a 赋值给成员变量 i j = b; // 将传入值 b 赋值给成员变量 j } 〜Samp() // 构造函数的析构函数 { cout<<"析构.."<<i<<endl; // 输出析构函数的消息 } int GetMuti() // 成员函数 GetMuti,返回 i 和 j 的积 { return i*j; // 返回 i 乘以 j 的结果 } protected: // 访问权限为 protected int i; // 类的成员变量 i int j; // 类的成员变量 j }; int main() // 程序入口函数 { Samp *p; // 定义指向 Samp 类对象的指针 p p = new Samp[5]; // 动态分配 5 个 Samp 对象的存储空间,并将对象首地址赋给指针 p if(!p) // 判断内存是否分配成功 { cout<<"内存分配错误 "; // 内存分配失败,输出错误信息 return 1; // 返回错误结果 1 } for(int j = 0;j<5;j++) // 使用循环语句为每一个 Samp 对象的私有成员变量 i 和 j 设置值 p[j].Setij(j,j); // 调用成员函数 Setij 为 Samp 对象的成员变量 i 和 j 赋值 for(int k = 0; k < 5; k++) // 使用循环语句输出每一个 Samp 对象的积 cout<<"Muti["<<k<<"]值是:"<<p[k]. GetMuti() <<endl; // 输出信息 delete [ ] p; // 释放动态分配的存储空间 return 0; // 返回成功状态 0 }
定义了一个类 Samp,包含了 Setij 和 GetMuti 两个成员函数,其中 Setij 用于设置私有成员变量 i 和 j 的值,GetMuti 用于返回 i 和 j 的积
变量 i 和 j 使用 protected 修饰,表示其可以被子类继承
在 main 函数中,定义了一个指向 Samp 数组的指针 p,通过 new 关键字动态分配了 5个 Samp 对象的内存,如果分配失败,则输出内存分配错误的信息并退出程序
使用循环语句为每一个 Samp 对象的私有成员变量 i 和 j 设置值
再次使用循环语句输出每一个 Samp 对象的积
最后使用 delete [ ] p 释放动态分配的内存空间
这段代码虽然简短,但涉及到了 C++ 中比较基础和重要的一些内容,包括动态内存分配、类的定义与调用、成员变量的存取控制等。
需要注意的是,由于 p 是一个指向 Samp 数组的指针,所以可以通过 p[k] 的形式来操作数组中的每一个 Samp 对象。在程序结束时需要 delete [ ] p 释放内存,否则会发生内存泄漏。
三、类的静态成员
(1)静态变量
① 概念
⚫ 与 C 语言一样,可以使用 static 说明自动变量
- 根据定义的位置不同,分为静态全局变量和静态局部变量
⚫ 全局变量是指在所有花括号之外声明的变量
- 全局变量的作用域范围是全局可见的,即在整个项目文件内都有效
- 使用 static 修饰的全局变量是静态全局变量,其作用域有所限制,仅在定义该变量的源文件内有效,项目中的其他源文件中不能使用它
⚫ 块内定义的变量是局部变量
- 从定义之处开始到本块结束处为止是局部变量的作用域
- 使用 static 修饰的局部变量是静态局部变量,即定义在块中的静态变量
- 静态局部变量具有局部作用域,但却具有全局生存期
⚫ 静态局部变量具有局部作用域,但却具有全局生存期
- 也就是说,静态局部变量在程序的整个运行期间都存在,它占据的空间一直到程序结束时才释放,但仅在定义它的块中有效,在块外并不能访问它
⚫ 静态变量均存储在全局数据区,静态局部变量只执行一次初始化
- 如果程序未显式给出初始值,则相当于初始化为 0
- 如果显式给出初始值,则在该静态变量所在块第一次执行时完成初始化
② 示例
以下代码主要演示了 C++ 静态变量的使用方法以及作用域问题:
/* 示例:静态变量的使用方法以及作用域问题 */ #include <iostream> // 引入头文件iostream(输入输出流库) using namespace std; // 使用命名空间std static int glos=100; // 定义全局静态变量glos并初始化为100 void f( ) // 声明一个无参无返回值的函数f { int a=1; // 在函数f内部定义并初始化一个自动变量a static int fs=1; // 在函数f内部定义并初始化一个静态局部变量fs cout<<"在f中:a(自动)="<<a<<" fs(静态)="<<fs<<" glos(静态)="<<glos<<endl; // 输出3个变量的值 a+=2; // 对自动变量a进行操作 fs+=2; // 对静态局部变量fs进行操作 glos+=10; // 对全局静态变量glos进行操作 cout<<"在f中:a(自动)="<<a<<" fs(静态)="<<fs<<" glos(静态)="<<glos<<endl<<endl; // 输出修改后的3个变量的值 //cout<<"ms="<<ms<<endl; // 注释掉的语句,调用变量ms时出错(变量作用域的问题) } int main( ) // 声明一个主函数 { static int ms =10; // 定义一个静态局部变量ms并初始化为10 for(int i=1;i<=3;i++) f(); // 循环3次,每次调用函数f输出变量的值 //cout<<"fs="<<fs<<endl; // 注释掉的语句,调用变量fs时出错(变量作用域的问题) cout<<"ms="<<ms<<endl; // 输出变量ms的值 cout<<"glos="<<glos<<endl; // 输出全局静态变量glos的值 return 0; // 返回成功状态0 }
- 在全局作用域中定义了一个静态变量 glos ,并且对其进行了初始化。
- 在函数 f 中定义了一个自动变量 a 和一个静态局部变量 fs,并且对 fs 进行了初始化。函数 f 中使用 cout 语句输出 a、fs、glos 的值,接着对 a、fs、glos 进行了一些操作,最后再次输出这三个变量的值。此外,注释掉的语句 cout<<“ms=”<<ms<<endl; 和 cout<<“fs=”<<fs<<endl; 处理了变量作用域。
- 在 main 函数中,定义了一个静态局部变量 ms 并对其进行了初始化,接着使用 for 循环调用函数 f,输出 a、fs、glos 的值。注释掉的语句 cout<<“fs=”<<fs<<endl; 从另一侧面说明了变量作用域的问题。
- 最后使用输出语句,输出 ms 和 glos 的值,并返回 0 表示程序正常结束。
- 需要注意的是,静态变量的作用域为整个程序,而自动变量和局部变量的作用域只限于函数内部。
- 静态变量在程序运行期间一直有效,直到程序结束,因此可以多次调用函数f,静态变量也不会被重复初始化。而自动变量和局部变量只在函数调用时存在,函数执行完毕后就会被销毁。
(2)类的静态成员
① 概念
⚫ 类的静态成员有两种:静态成员变量和静态成员函数。
- 在类体内定义类的成员时,在前面添加 static 关键字后,该成员即成为静态成员
⚫ 类的静态成员被类的所有对象共享,不论有多少对象存在,静态成员都只有一份保存在公用内存中。
- 对于静态成员变量,各对象看到的值是一样的
⚫ 定义类静态成员变量时,在类定义中声明静态成员变量,然后 必须在类体外定义静态成员变量的初值。
- 这个初值不能在类体内赋值
⚫ 给静态成员变量赋初值的格式如下:
- 类型 类名::静态成员变量=初值;
⚫ 注意, 在类体外为静态成员变量赋初值时,前面不能加 static 关键字, 以免和一般的静态变量相混淆。
- 在类体外定义成员函数时,前面也不能加 static 关键字
② 说明
- 对于普通成员变量,每个对象有各自的一份,而静态成员变量只有一份,被同类所有对象共享。
- 普通成员函数一定是作用在某个对象上的,而静态成员函数并不具体作用在某个对象上。
- 访问普通成员时,要通过 “对象名.成员名” 等方式,指明要访问的成员变量是属于哪个对象的,或要调用的成员函数作用于哪个对象;访问静态成员时,则可以通过 “类名::成员名” 的方式访问,不需要指明被访问的成员属于哪个对象或作用于哪个对象。
- 因此,甚至可以在还没有任何对象生成时就访问一个类的静态成员。
- 非静态成员的访问方式其实也适用于静态成员,也就是可以通过 “对象名.成员名” 的方式访问,效果和 “类名::成员名” 这种访问方式没有区别。
③ 示例
以下代码主要演示了 C++ 类的静态数据成员和静态成员函数的使用方法:
/* 示例:静态数据成员和静态成员函数的使用 */ class Test // 声明一个名为Test的类 { static int x; // 声明一个静态数据成员x,类型为int int n; // 声明一个非静态数据成员n,类型为int public: // 访问权限public Test(){} // 定义无参数的Test类的构造函数 Test(int a,int b){x=a;n=b;} // 定义含两个参数的Test类的构造函数Test为内联函数 static int func( ){return x;} // 定义静态成员函数func为内联函数,返回静态数据成员x的值 static void sfunc(Test&r,int a){r.n=a;} // 定义静态成员函数sfunc为内联函数,函数以Test类的引用r和整形数a为参数 int Getn(){return n;} // 定义非静态成员函数Getn为内联函数,返回非静态数据成员n的值 }; // 类Test的声明结束 int Test::x=25; // 初始化静态数据成员x,赋初始值为25 #include <iostream> // 引入头文件iostream(输入输出流库) using namespace std; // 使用命名空间std void main( ) // 声明一个主函数 { cout<<Test::func( ); // 输出Test类中静态数据成员x的值,此时输出25 Test b,c; // 声明两个Test类的对象b和c,使用无参数的构造函数初始化 b.sfunc(b,58); // 修改对象b的非静态数据成员n的值为58,r为b的引用 cout<<" "<<b.Getn( ); // 输出修改后的对象b的非静态数据成员n的值58 cout<<" "<<b.func( ); // 输出静态数据成员x的值25,属于Test类,所有对象共享 cout<<" "<<c.func( ); // 输出静态数据成员x的值25,属于Test类,所有对象共享 Test a(24,56); // 创建一个Test类的对象a,变量x的值为24,非静态数据成员n的值为56 cout<<" "<<a.func( )<<" "<<b.func( )<<" "<<c.func( )<<endl; // 输出a.func()、b.func()和c.func()三个静态成员函数的值 } // 主函数结束 /* 执行结果 */ 25 58 25 25 24 24 24
定义了一个名为 Test 的类,在类的定义中声明了静态数据成员 x 、非静态数据成员 n 和相关成员函数,其中包括构造函数、静态成员函数和非静态成员函数。
在类外部对静态成员变量 x 进行了初始化,将其赋值为 25 。
在 main 函数中,先使用 Test::func() 输出静态数据成员 x 的值,接着使用无参数的构造函数 Test() 分别创建了对象 b、c 和 a ,其中 b 使用了 b.sfunc(b,58) 函数对其非静态数据成员 n 的值进行修改,最后输出了 b.func()、c.func() 和 a.func() 的值。
需要注意的是,静态数据成员和静态成员函数不属于任何一个对象,而是属于整个类的,因此可以通过类名::的形式访问它们,无需创建对象。
另外,静态成员函数只能访问静态数据成员和其他静态成员函数,不能访问非静态数据成员和非静态成员函数。
以下代码主要演示了 C++ 类的静态数据成员使用方法:
/* 示例:静态数据成员的使用 */ class A // 声明一个名为A的类 { int i,j; // 声明两个非静态数据成员i和j,类型均为int static int x,y; // 声明两个静态数据成员x和y,类型均为int public: // 访问权限public A(int a=0,int b=0,int c=0, int d=0){i=a;j=b;x=c;y=d;} // 定义含4个参数(两个非静态数据成员i和j,两个静态数据成员x和y)的构造函数 void Show() // 定义一个成员函数Show,返回值为void,无参数 { cout << "i="<<i<<' '<<"j="<<j<<' '; cout << "x="<<x<<' '<<"y="<<y<<' '; } }; // 类A的声明结束 int A::x=0; // 对静态数据成员x进行一次定义性说明,将其初始值设为0 int A::y=0; // 对静态数据成员y进行一次定义性说明,将其初始值设为0 void main(void) // 声明一个主函数 { A a(2,3,4,5); // 创建一个A类的对象a,含4个参数值 a.Show(); // 输出对象a的各个数据成员的值 A b(100,200,300,400); // 创建另一个A类的对象b,含4个参数值 b.Show(); // 输出对象b的各个数据成员的值 a.Show(); // 再次输出对象a的各个数据成员的值 } // 主函数结束 /* 说明: a.x 和b.x在内存中占据一个空间 a.y 和b.y在内存中占据一个空间 */ /* 执行结果 */ i=2 j=3 x=4 y=5 i=100 j=200 x=300 y=400 i=2 j=3 x=300 y=400
定义了一个名为 A 的类,其中包括四个数据成员:i、j 为非静态数据成员,x、y 为静态数据成员。A 类中的构造函数用于初始化数据成员。
在类外部对静态数据成员 x、y 进行了一次定义路径说明,将它们的值初始化为 0 。
在 main 函数中,首先使用构造函数创建一个对象 a ,并调用 a.Show() 函数输出对象 a 中所有的数据成员的值。接着再创建一个对象 b ,也调用其 Show() 函数输出对象b的各数据成员的值。最后再次调用 a.Show() 函数,输出对象 a 的各数据成员的值。
需要注意的是,静态数据成员属于整个类,其值只有一个,所有该类的对象都共享它。而非静态数据成员一般属于对象,其各个对象间的值是不同的。
当需要对静态数据成员进行定义路径说明时,可以在类外部进行,也可以在类内部使用 static 关键字进行。
以下代码主要演示了 C++ 类的静态数据成员的使用方法以及使用类名限定符直接访问静态数据成员:
/* 示例:静态数据成员的使用方法以及使用类名限定符直接访问静态数据成员 */ #include <iostream.h> // 引入头文件<iostream.h>(输入输出流库) class A // 声明一个名为A的类 { int i,j; // 声明两个非静态数据成员i和j,类型均为int public: // 访问权限public static int x,y; // 声明两个静态数据成员x和y,类型均为int public: // 访问权限public A(int a=0,int b=0,int c=0, int d=0){i=a;j=b;x=c;y=d;} // 定义含4个参数(两个非静态数据成员i和j,两个静态数据成员x和y)的构造函数 void Show() // 定义一个成员函数Show,返回值为void,无参数 { cout << "i="<<i<<' '<<"j="<<j<<' ';// 输出非静态数据成员i和j的值 cout << "x="<<x<<' '<<"y="<<y<<" ";// 输出静态数据成员x和y的值 } }; int A::x=1000; // 对静态数据成员x进行一次定义性说明,将其初始值设为1000 int A::y=1000; // 对静态数据成员y进行一次定义性说明,将其初始值设为1000 void main(void) // 声明一个主函数 { cout<<"A::x="<<A::x<<" A::y="<<A::y<<endl; // 输出A类的静态数据成员x和y的初始值 A a(2,3,4,5); // 创建一个A类的对象a,含4个参数值 a.Show(); // 输出对象a的各个数据成员的值 A b(100,200,300,400); // 创建另一个A类的对象b,含4个参数值 b.Show(); // 输出对象b的各个数据成员的值 a.Show(); // 再次输出对象a的各个数据成员的值 cout<<"A::x="<<A::x<<" A::y="<<A::y<<endl; // 输出A类的静态数据成员x和y的值 } // 主函数结束 /* 执行结果 */ A::x=1000 A::y=1000 i=2 j=3 x=4 y=5 i=100 j=200 x=300 y=400 i=2 j=3 x=300 y=400 A::x=300 A::y=400
定义了一个名为 A 的类,其中包括四个数据成员:i、j 为非静态数据成员,x、y 为静态数据成员。其中构造函数用于初始化对象的各数据成员。
在类外部对静态数据成员 x、y 进行了一次定义性说明,将它们的值初始化为 1000。
在 main 函数中,先使用 cout 输出 A 类的静态数据成员 x 和 y 的初始值,接着创建一个对象 a ,再创建一个对象 b ,调用它们各自的 Show() 函数输出对象中的所有数据成员的值。接着再次调用 a.Show() 函数。最后再次使用 cout 输出 A 类的静态数据成员 x 和 y 的值。
需要注意的是,静态数据成员属于整个类,其值只有一个,所有该类的对象都共享它。
使用类名限定符直接访问静态成员可以不需要创建任何对象实例,直接使用 “类名::静态成员” 的方式即可。
以下代码演示了 C++ 类的静态数据成员,以及如何通过类名限定符直接访问静态数据成员:
/* 示例:类的静态数据成员和类名限定符的使用 */ class A // 声明一个名为A的类 { int i,j; // 声明两个非静态数据成员i,j,类型均为int public: // 访问权限public static int x; // 声明一个静态数据成员x,类型为int public: A(int a=0,int b=0,int c=0){ i=a ; j=b ; x=c; } // 定义含3个参数(两个非静态数据成员i和j,一个静态数据成员x)的构造函数 void Show() // 定义一个成员函数Show,返回值为void,无参数 { cout << "i="<<i<<' '<<"j="<<j<<' '; // 输出非静态数据成员i和j的值 cout << "x="<<x<<" "; // 输出静态数据成员x的值 } }; int A::x=500; // 对静态数据成员x进行一次定义性说明,将其初始值设为500 void main(void ) // 声明一个主函数 { A a(20,40,10),b(30,50,100); // 创建两个A类的对象a和b,分别含有不同的三个参数值 a.Show(); // 输出对象a的各个数据成员的值 b.Show(); // 输出对象b的各个数据成员的值 cout << "A::x=" << A::x << ' '; // 输出A类的静态数据成员x的值,可以直接用类名引用 } // 主函数结束
定义了一个名为A的类,其中包括三个数据成员:i、j 为非静态数据成员,x 为静态数据成员。其中构造函数用于初始化对象的各数据成员。
在类外部对静态数据成员 x 进行了一次定义性说明,将它的值初始化为 500。
在 main 函数中,创建了两个 A 类的对象 a 和 b ,分别含有不同的三个参数值,调用对象的 Show 函数输出对象中的所有数据成员的值。然后使用 cout 输出 A 类的静态数据成员 x 的值。
需要注意的是,静态数据成员属于整个类,其值只有一个,所有该类的对象都共享它。
使用类名限定符直接访问静态数据成员可以不需要创建任何对象实例,直接使用 “类名::静态成员” 的方式即可。
(3)格式
- 访问静态成员时,成员前面既可以用类名作前缀,也可以使用对象名或对象指针作前缀。
- 这与访问类成员时仅能使用对象名或对象指针作前缀是不同的。
访问类静态成员的一般格式如下:
【格式一】
- 类名::静态成员名
【格式二】
- 对象名.静态成员名
【格式三】
- 对象指针->静态成员名
【特别注意】
- 类的静态成员函数没有 this 指针,不能在静态成员函数内访问非静态的成员
- 即通常情况下,类的静态成员函数只能处理类的静态成员变量
- 静态成员数内函也不能调用非静态成员函数
四、变量及对象的生存期和作用域
⚫ 变量的生存期是指变量所占据的内存空间由分配到释放的时期。
- 变量有效的范围称为其作用域
- 全局变量是程序中定义在所有函数(包括 main 函数)之外的任何变量,其作用域是程序从变量定义到整个程序结束的部分。
- 这意味着全局变量可以被所有定义在全局变量之后的函数访问。
- 全局变量及静态变量分配的空间在全局数据区,它们的生存期为整个程序的执行期间。
⚫ 而局部变量,如在函数内或程序块内说明的变量,被分配到局部数据区,如栈区等。
- 这种分配是临时的,一旦该函数体或程序块运行结束,所分配的空间就会被撤销。
- 局部变量的生存期从被说明处开始,到所在程序块结束处结束。
⚫ 对于静态变量,如果没有进行初始化,系统会自动初始化为 0 。
- 局部变量如果没有进行初始化,则其值是不确定的。
⚫ 使用 new 运算符创建的变量具有动态生存期。
- 从声明处开始,直到用 delete 运算符释放存储空间或程序结束时,变量生存期结束。
⚫ 类的对象在生成时调用构造函数,在消亡时调用析构函数,在这两个函数调用之间即是对象的生存期。
五、常量成员和常引用成员
(1)概念
⚫ 在类中,也可以使用 const 关键字定义成员变量和成员函数,甚至是类的对象。
- 由关键字 const 修饰的类成员变量称为类的常量成员变量。
- 类的常量成员变量必须进行初始化,而且只能通过构造函数的成员初始化列表的方式进行。
- 使用 const 修饰的函数称为常量函数。
- 定义类的对象时如果在前面添加 const 关键字,则该对象称为常量对象。
⚫ 定义常量对象或常量成员变量的一般格式为:
- const 数据类型 常量名=表达式;
⚫ 定义常量函数的格式如下:
- 类型说明符 函数名(参数表)const;
⚫ 在对象被创建以后,其常量成员变量的值就不允许被修改,只可以读取其值。
- 对于常量对象,只能调用常量函数。
- 总之,常量成员变量的值不能修改,常量对象中的各个属性值均不能修改。
(2)示例
【注意】
- 说明常量对象后,不能通过常量对象调用普通成员函数
【示例】使用常量对象不能调用非常量成员函数
/* 示例:定义了一个名为 CDemo 的类,具有一个公共的成员函数 SetValue() */ class CDemo { public: void SetValue(){} // 声明公有成员函数SetValue };
- 第一行定义了 CDemo 这个类
- 第二行声明了一个公有的成员函数 SetValue()。在 C++ 中,成员函数可以分为公有和私有两类,公有成员函数可以在类的外部直接访问,私有成员函数仅限于在类的内部使用。此处的 SetValue() 成员函数没有任何参数,返回值为 void ,花括号内没有具体实现,表示该函数是一个空函数。
- 需要注意的是,类的定义必须以分号结束。
- 另外,成员函数的定义通常需要写在类的外部,即需要单独写一个源代码文件,在其中实现成员函数的具体功能。完成函数定义后,在类声明中声明函数的名称和参数。这种定义方法分离了类的声明和定义,方便管理和扩展。
【示例】定义了一个名为 Obj 的常量 CDemo 对象,然后调用了它的 SetValue() 成员函数
const CDemo Obj; // 声明一个名为 Obj 的常量 CDemo 对象:Obj 是常量对象 // 错误代码示例: Obj.SetValue(); // 错误!SetValue() 函数需要一个参数 // 错误代码修正: Obj.SetValue(10); // 正确!传入一个 int 型的参数给 SetValue() 函数 /* 正确的代码应该要为 SetValue() 函数提供一个 int 型的参数,表示要给对象赋的值。 然而,因为 Obj 是一个常量对象,其数据成员无法再被修改,所以 SetValue() 函数应该被声明为 const 函数,以确保不会修改 Obj 的值。 这样,SetValue() 函数就不会修改 Obj 的值,代码也就不会报错了, 即: */ class CDemo { public: void SetValue(int value) const {}; // 声明公有 const 成员函数 SetValue };
六、成员对象和封闭类
(1)成员对象和封闭类
- 一个类的成员变量如果是另一个类的对象,则该成员变量称为“成员对象” 。
- 这两个类为包含关系。
- 包含成员对象的类叫作封闭类。
【示例】
- 有类 A 和类 B , 在类 B 中定义了一个成员变量 v,v 的类型是类 A
- 或者在类 B 中定义了一个函数,返回值类型是类A
- 则类 A 和类 B 是包含关系,更确切地说,类 B 包含类 A ,类 B 即是封闭类
(2)封闭类构造函数的初始化列表
- 当生成封闭类的对象并进行初始化时,它包含的成员对象也需要被初始化,需要调用成员对象的构造函数。
- 在定义封闭类的构造函数时,需要添加初始化列表,指明要调用成员对象的哪个构造函数。
【格式】 在封闭类构造函数中添加初始化列表的格式如下:封闭类名::构造函数名(参数表): 成员变量1(参数表),成员变量2(参数表),…{…}【说明】
- 初始化列表中的成员变量既可以是成员对象,也可以是基本数据类型的成员变量。
- 对于成员对象,初始化列表的“参数表”中列出的是成员对象构造函数的参数(它指明了该成员对象如何初始化)。
- 先调用成员对象的构造函数,再调用封闭类对象的构造函数。
(3)封闭类的复制构造函数
如果封闭类的对象是用默认复制构造函数初始化的,那么它包含的成员对象也会用复制构造函数初始化。
七、友元
(1)友元
- 友元实际上并不是面向对象的特征,而是为了兼顾 C 语言程序设计的习惯与 C++ 友元的概念破坏了类的封装性和信息隐藏,但有助于数据共享,能够提高程序执行的效率。
- 信息隐藏的特点,而特意增加的功能。
- 这是一种类成员的访问权限。
- 友元使用关键字 friend 标识。
- 在类定义中,当 friend 出现在函数说明语句的前面时,表示该函数为类的友元函数。
- 一个函数可以同时说明为多个类的友元函数,一个类中也可以有多个友元函数。
- 当 friend 出现在类名之前时,表示该类为类的友元类。
(2)友元函数
- 在定义一个类的时候,可以把一些函数(包括全局函数和其他类的成员函数)声明为 “友 元” ,这样那些函数就成为本类的友元函数。
- 在友元函数内部可以直接访问本类对象的私有成员。
【格式一】在类定义中,将一个全局函数声明为本类友元函数的格式如下:
- friend 返回值类型 函数名(参数表);
【格式二】当有某类A的定义后,将类A的成员函数说明为本类的友元函数的格式如下:
- friend 返回值类型 类A::类A的成员函数名(参数表);
【说明】
- 不能把其他类的私有成员函数声明为友元函数。
- 友元函数不是类的成员函数,但允许访问类中的所有成员。
- 在函数体中访问对象成员时,必须使用“对象名.对象成员名”的方式。
- 友元函数不受类中的访问权限关键字限制,可以把它放在类的公有、私有、保护部分,结果是一样的。
(3)友元类
如果将一个类 B 说明为另一个类 A 的 友元类 ,则类 B 中的所有函数都是类 A 的友元函数,在类 B 的所有成员函数中都可以访问类 A 中的所有成员。
【格式】在类定义中声明友元类的格式如下:
- friend class 类名;
【说明】
- 友元类的关系是单向的。
- 若说明类 B 是类 A 的友元类,不等于类 A 也是类 B 的友元类。
- 友元类的关系不能传递,即若类 B 是类 A 的友元类,而类 C 是类 B 的友元类,不等于类 C 是类 A 的友元类。
- 除非确有必要,一般不把整个类说明为友元类,而仅把类中的某些成员函数说明为友元函数。
(4)示例
/* 示例:定义了一个像素点类Pixel和一个测试类Test,并实现了一些成员函数和全局函数 */
#include<iostream>
#include<cmath>
using namespace std;
class Pixel; // 前向声明Pixel类,因为Test类中需要用到Pixel类
class Test
{
public:
void printX(Pixel p); // 声明一个成员函数printX,参数为Pixel类型对象p
};
class Pixel // 声明Pixel类
{
private:
int x,y; // 私有数据成员x和y
public:
// 构造函数
Pixel(int x0, int y0) // 声明构造函数
{
x=x0;
y=y0;
}
// 打印像素点坐标的函数
void printxy() // 声明一个成员函数printxy,用来输出Pixel对象的x、y值
{
cout<<"pixel:("<<x<<","<<y<<")"<<endl;
}
// 友元函数声明(用来计算两个像素点之间的距离)
friend double getDist(Pixel p1,Pixel p2); // 声明一个友元函数getDist,用来计算两个Pixel对象之间的距离
// 友元函数声明(用来输出像素点的x、y值)
friend void Test::printX(Pixel p); // 声明一个友元函数printX,用来输出Pixel对象的x、y值
};
void Test::printX(Pixel p)
{
cout<<"x="<<p.x<<" y="<<p.y<<endl; // 实现printX函数,输出Pixel对象的x、y值
return;
}
double getDist(Pixel p1,Pixel p2) // 实现getDist函数,用来计算两个Pixel对象之间的距离
{
double xd=double(p1.x-p2.x);
double yd=double(p1.y-p2.y);
return sqrt(xd*xd+yd*yd);
}
int main()
{
Pixel p1(0,0),p2(10,10); // 创建两个Pixel对象p1和p2
p1.printxy(); // 输出p1的xy值
p2.printxy(); // 输出p2的xy值
cout<<"(p1,p2)间距离="<<getDist(p1,p2)<<endl; // 输出p1和p2之间的距离
Test t; // 创建一个Test对象t
cout<<"从友元函数中输出--"<<endl;
t.printX(p1); // 输出p1的xy值
t.printX(p2); // 输出p2的xy值
return 0;
}
【代码详解】
1. Pixel 类表示一个像素点。它有私有成员变量x和y,构造函数 Pixel(int, int) 可以接收 x 和 y 的值并将它们设置为对象的数据成员。Pixel 还有一个成员函数 printxy() 用来输出这个像素点的坐标。
class Pixel { private: int x, y; public: Pixel(int x0, int y0) { // 构造函数 x = x0; y = y0; } void printxy() { // 打印像素点坐标的函数 cout << "pixel:(" << x << "," << y << ")" << endl; } // 友元函数声明(用来计算两个像素点之间的距离) friend double getDist(Pixel p1, Pixel p2); // 友元函数声明(用来输出像素点的x、y值) friend void Test::printX(Pixel p); };
2. 在 Pixel 类中,有两个友元函数被声明。由关键字 “friend” 所标识,这两个函数具有访问 Pixel 类的私有成员变量 x 和 y 的权限。这两个函数分别是:
friend double getDist(Pixel p1, Pixel p2); friend void Test::printX(Pixel p);
- 其中第一个函数 getDist() 被实现成一个全局函数。它可以计算两个像素点 p1 和 p2 之间的距离,使用勾股定理计算得到两个像素点在平面上的直线距离。
- 第二个函数 printX() 被实现成了 Test 类的成员函数。这个函数通过一个 Pixel 对象作为参数,输出这个对象的 x、y 坐标值。
3. Test 类是一个测试像素点类 Pixel 的类。它有一个成员函数 printX() ,通过一个像素点对象作为参数,输出这个对象的 x、y 坐标值。这个函数需要在 Test 类的外部实现,因为它是类外的一个成员函数。
class Test { public: void printX(Pixel p); // 声明一个成员函数printX,参数为Pixel类型对象p }; void Test::printX(Pixel p) { // 实现函数printX,输出Pixel对象的x和y值 cout << "x=" << p.x << " y=" << p.y << endl; return; }
4. getDist() 是一个全局函数,用来计算两个给定像素点之间的距离。上文中的友元函数getDist() 和全局函数 getDist() 的定义一致。
double getDist(Pixel p1, Pixel p2) { double xd = double(p1.x - p2.x); double yd = double(p1.y - p2.y); return sqrt(xd * xd + yd * yd); // 勾股定理计算两个像素点的距离 }
5. 在main()函数中,我们创建了两个像素点p1和p2,并分别输出它们的x、y坐标值,以及计算了它
int main() { Pixel p1(0, 0), p2(10, 10); // 创建两个像素点p1和p2 p1.printxy(); // 输出p1的x、y坐标值 p2.printxy(); // 输出p2的x、y坐标值 cout << "(p1,p2)间距离=" << getDist(p1, p2) << endl; // 计算p1和p2之间的距离 Test t; // 创建一个Test对象t cout << "从友元函数中输出--" << endl; t.printX(p1); // 从友元函数中输出p1的x、y坐标值 t.printX(p2); // 从友元函数中输出p2的x、y坐标值 return 0; }
八、this 指针
(1)this 指针
- C++ 语言规定,当调用一个成员函数时,系统自动向它传递一个隐含的参数。该参数是一个指向调用该函数的对象的指针,称为 this 指针,从而使成员函数知道对哪个对象进行操作。
- C++ 规定,在非静态成员函数内部可以直接使用 this 关键字,this 就代表指向该函数所作用的对象的指针。
- 在一般情况下,在不引起歧义时,可以省略 “this->” ,系统采用默认设置。
- 静态成员是类具有的属性,不是对象的特征,this 表示的是隐藏的对象的指针,所以静态成员函数没有 this 指针。
(2)示例
以下代码定义了一个名为 myDate 的类(无 this 指针):
class myDate { public: myDate0, myDate(int,int, int); ... private: int year,month, day; }; my Date::myDate() { year = 1970; month = 1; day = l; } myDate::myDate(int y, int m, int d) { year = y; month = m; day = d; } ...
- myDate 的类有两个构造函数,和三个私有数据成员:year、month、day
- 第一个构造函数是默认构造函数 (myDate0) ,它没有接收任何参数,当该构造函数被调用时,将会把对象的 year 设置为 1970,month 设置为 1,day 设置为 1。
- 第二个构造函数接收三个整型变量 y、m 和 d 做为参数,分别代表年、月、日。在该构造函数内,年、月、日的值等于输入的值。
- 需要注意的是,由于 month、day、year 都是私有成员,无法在类的外部被直接访问。只能通过函数成员来实现对这些私有成员变量的访问。
以下代码是 myDate 类的构造函数的定义(有 this 指针):
class myDate { public: myDate0, myDate(int,int, int); ... private: int year,month, day; }; my Date::myDate() { year = 1970; month = 1; day = l; } myDate::myDate(int year, int month, int day) { this->year = year; this->month = month; this->day = day; }
- 它有三个参数:year、month 和 day,分别表示年、月、日。这个构造函数被定义为类的成员函数,因此它有一个额外的隐藏参数 this 指针,指向被构造的对象本身。
- 在函数定义体内,使用了 this 指针来访问对象的成员变量 year、month 和 day,并将它们的值分别设置为传递进来的参数 year、month 和 day 的值。
- this 指针可以理解为一个隐含的参数,由编译器自动添加。当对象执行成员函数时,编译器会将该对象的地址作为this指针传递给成员函数,使得在成员函数内部可以访问对象的成员变量。这样就可以在成员函数内部区分对象的成员变量和局部变量。在这个构造函数中,成员变量和局部变量名字相同,使用 this 指针可以解决冲突。