构造函数
变量 | 初始化 |
---|---|
全局变量 | 如果程序员在声明变量时没有进行初始化, 则系统自动为其初始化为0。 这个工作在程序启动时完成。 |
局部变量 | 系统不进行自动初始化, 所以它的处置需要靠程序员给定。 如果程序员没有设定, 则是一个随机值。 |
- 为了对对象进行初始化,c++提供了一种称为构造函数的机制,用于对对象进行初始化,实际上是用来为成员变量赋初值的。
- 构造函数是类中的特殊成员函数,它属于类的一部分。给出类定义时,由程序员编写构造函数。如果程序员没有编写类的任何构造函数,则由系统自动添加一个不带参数的构造函数。
- 声明对象后,可以使用
new
运算符为对象进行初始化,此时调用的是对象所属类的构造函数。构造函数的作用是完成对象的初始化工作,用来保证对象的初始状态是确定的。在对象生成时,系统自动调用构造函数,用户在程序中不会直接调用构造函数。
构造函数的定义
定义一个类时,需要为类定义相应的构造函数。构造函数的函数名与类名相同,没有****返回值****。一个类的构造函数可以有多个,即****构造函数允许重载。同一个类的多个构造函数的参数表一定不能完全相同。
构造函数的声明格式如下:
类名(形参1, 形参2, ..., 形参n);
- 在声明类的构造函数时可以同时给出函数体,这样的构造函数称为内联函数。也可以在类体外给出构造函数的定义。构造函数的声明中,形参的个数可以为0,即参数表为空。
- 当类中没有定义任何构造函数时,系统会自动添加一个参数表为空、函数体也为空的构造函数,称为默认构造函数。所以任何类都可以保证至少有一个构造函数。
- 如果程序员在程序中已经定义了构造函数,则系统不会再添加默认构造函数。
假设类的成员变量有三个,则在类体外定义构造函数时通常有如下3中形式:
//方式一
//使用带入的参数值通过初始化列表为各成员变量赋初值
Student::Student(int sno, int age, string name):_sno(sno), _age(age), _name(name) {
}
//或者
//使用固定值在初始化列表中为个成员变量赋初值
Student::Student():_sno(11111), _age(13), _name("slh") {
}
//方式二
Student::Student(int sno, int age, string name) {
_sno = sno;
_age = age;
_name = name;
}
//方式三
Student::Student() {
_sno = 12345;
_age = 12;
_name = "tyg";
}
再比如类Student
已经声明了下列4个构造函数:
class Student {
public:
Student();
Student(int);
Student(int, int);
Student(int, int, string);
void printStudent();
private:
int _sno;
int _age;
string _name;
};
在类体外定义构造函数
Student::Student():_sno(12321), _age(23), _name("df") {
}
Student::Student(int sno):_age(12), _name("dfs") {
_sno = sno;
}
Student::Student(int sno, int age):_name("dsf") {
_sno = sno;
_age = age;
}
Student::Student(int sno, int age, string name) {
_sno = sno;
_age = age;
_name = name;
}
构造函数的使用
C++语言规定,创建类的任何对象时都一定会调动构造函数进行初始化。对象需要占据内存空间,生成对象时,为对象分配的这段内存空间的初始化由构造函数完成。
特别地,如果程序中声明了对象数组,即数组的每个元素都是一个对象,则一定要为对象所属的这个类定义一个无参的构造函数。因为数组中每个元素都需要调用无参的构造函数进行初始化,所以必须要有一个不带参数的构造函数。
复制构造函数与类型转换构造函数
复制构造函数是构造函数的一种,也称为拷贝构造函数。它的作用是使用一个已存在的对象去初始化另一个正在创建的对象。例如,类对象间的赋值是由复制构造函数实现的。
复制构造函数只有一个参数,参数类型是****本类的引用。复制构造函数的参数可以是const
引用,也可以是非const
引用。一个类中可以写两个复制构造函数,一个函数的参数是const
引用,另一个函数的参数是非const
引用。这样,当调用复制构造函数时,既能以常量对象(初始化后值不能改变的对象)作为参数,也能以非常量对象作为参数去初始化其他对象。对于类A而言,复制构造函数的原型如下:
//格式一
A::A(const A &)
//格式二
A::A(A &)
例如:
//复制构造函数
Student::Student(Student &s) {
_sno = s._sno;
_age = s._age;
_name = s._name;
}
Student::Student(const Student &s) {
_sno = s._sno;
_age = s._age;
_name = s._name;
}
自动调用复制构造函数的情况有以下3种:
- 当用一个对象去初始化本类的另一个对象时,会调用复制构造函数。例如,使用下列形式的说明语句时,即会调用复制构造函数。
- 类名 对象名2(对象名1);
- 类名 对象名2 = 对象名1;
-
如果函数
F
的参数是类A
的对象,那么当调用F
时,会调用类A
的复制构造函数。换句话说,作为形参的对象,是用复制构造函数初始化的,而且调用复制构造函数时的参数,就是调用函数时所给的实参。 -
如果函数的返回值是类
A
的对象,那么当函数返回时,会掉用类A
的复制构造函数。也就是说,作为函数返回值的对象是用复制构造函数初始化的,而调用复制构造函数时的实参,就是return
语句所返回的对象。
注意:
在复制构造函数的参数表中,加上const
是更好的做法。这样复制构造函数才能接收常量对象作为参数,即才能以常量对象作为参数去初始化别的对象。
析构函数
- 与构造函数一样,析构函数也是成员函数的一种,它的名字也与类名相同,但要在类名前面加一个
~
字符,以区别于构造函数。析构函数没有参数,也没有返回值。一个类中有且仅有一个析构函数,如果成员中没有定义析构函数,则编译器自动生成默认的析构函数。析构函数不可以多于一个,不会有重载的析构函数。默认析构函数的函数体为空。 - 创建对象时自动调用构造函数,那么,什么时候调用析构函数呢?可想而知,在对象消亡时自动调用析构函数。析构函数的作用是做一些善后处理的工作。例如,如果在创建对象时使用
new
运算符动态分配了内存空间,则在析构函数中应该使用delete
释放掉这部分占用的空间,保证空间可再利用。 - 当使用
new
运算符生成对象指针时,自动调用本类的构造函数。使用delete
删除这个对象时,首先为这个动态对象调用本类的析构函数,然后再释放这个动态对象占用的内存。
Student::~Student() {
cout << "student对象被释放了" << endl;
}
类的静态成员
与C语言一样,可以使用
static
说明自动变量。根据定义的位置不同,分为静态全局变量和静态局部变量。全局变量是指在所有花括号之外声明的变量,其作用域范围是全局可见的,即在整个项目文件内都有效。使用
static
修饰的全局变量是静态全局变量,其作用域有所限制,仅在定义该变量的源文件内有效,项目中的其他源文件中不能使用它。块内定义的变量是局部变量,从定义之处开始到本块结束处为止是局部变量的作用域。使用
static
修饰的局部变量是静态局部变量,即定义在块中的静态变量。静态局部变量具有局部作用域,但却具有全局生存期。静态局部变量具有局部作用域,但却具有全局生存期。也就是说,静态局部变量在程序的整个运行期间都存在,它占据的空间一直到程序结束时才释放,但仅在定义它的块中有效,在块外并不能访问它。
静态变量均存储在全局数据区,静态局部变量只执行一次初始化。如果程序未显式给出初始值,则相当于初始化为
0
;如果显式给出初始值,则在该静态变量所在块第一次执行时完成初始化。类的静态成员有两种:静态成员变量和静态成员函数。在类体内定义类的成员时,在前面添加
static关键字
后,该成员即成为静态成员。类的静态成员被类的所有对象共享,不论有多少对象存在,静态成员都只有一份保存在公用内存中。对于静态成员变量,各对象看到的值是一样的。
定义类静态成员变量时,在类定义中声明静态成员变量,然后必须在类体外定义静态成员变量的初值。这个初值不能在类体内赋值。
-
给静态成员变量赋初值的格式如下:
类型 类名::静态成员变量 = 初值;
注意:
在类体外为静态成员变量赋初值时,前面不能加static
关键字,以免和一般的静态变量想混淆。在类体外定义成员函数时,前面也不能加static
关键字。
访问静态成员时,成员前面既可以用类名作前缀,也可以使用对象名或对象指针作前缀。这与访问类成员时仅能使用对象名或对象指针作前缀是不同的。
//访问类静态成员的一般格式如下:
类名::静态成员名
//或者
对象名.静态成员名
//或者
对象指针->静态成员名
类的静态成员函数没有this
指针,不能在静态成员函数内访问非静态的成员,即通常情况下,类的静态成员函数只能处理类的静态成员变量。惊天成员函数内也不能调用非静态成员函数。
- 对于普通成员变量,每个对象有各自的一份,而****静态成员变量只有一份,被同类所有对象共享。
- 普通成员函数一定是作用在某个对象上的,而静态成员函数并不具体作用在某个对象上。
- 访问普通成员时,要通过
对象名.成员名
等方式,指明要访问的成员变量是属于哪个对象的,或要调用的成员函数作用于哪个对象; - 访问静态成员时,则可以通过
类名::成员名
的方式访问,不需要指明被访问的成员属于哪个对象或作用于哪个对象。因此,甚至可以在还没有任何对象生成时就访问一个类的静态成员。 - 非静态成员的访问方式其实也适用于静态成员,也就是可以通过
对象名.成员名
的方式访问,效果和类名::成员名
这种访问方式没有区别。
变量及对象的生存期和作用域
变量的生存期和作用域
- 变量的生存期是指变量所占据的内存空间由分配到释放的时期。变量有效的范围称为其作用域。全局变量是程序中定义在所有函数(包括
main
函数)之外的任何变量,其作用域是程序从变量定义到整个程序结束的部分。这意味着全局变量可以被所有定义在全局变量之后的函数访问。全局变量及静态变量分配的空间在全局数据区,它们的生存期为整个程序的执行期间。 - 而局部变量,如在函数内或程序块内说明的变量,被分配到局部数据区,如栈区等。这种分配是临时的,一旦该函数体或程序块运行结束,所分配的空间就会被撤销。局部变量的生存期从被说明处开始,到所在程序块结束处结束。
- 对于静态变量,如果没有进行初始化,系统会自动初始化为0。局部变量如果没有进行初始化,则其值是不确定的。
- 使用
new
运算符创建的变量具有动态生存期。从声明处开始,直到用delete
运算符释放存储空间或程序结束时,变量生存期结束。
类对象的生存期和作用域
类的对象在生成时调用构造函数,在消亡时调用析构函数,在这两个函数调用之间即是对象的生存期。
常量成员和常引用成员
-
在类中,也可以使用
const
关键字定义成员变量和成员函数,甚至是类的对象。由关键字const
修饰的类成员变量称为类的常量成员变量。类的常量成员变量必须进行初始化,而且只能通过构造函数的成员初始化列表的方式进行。使用const
修饰的函数称为常量函数。定义类的对象时如果在前面添加const
关键字,则该对象称为常量对象。定义常量对象或常量成员变量的一般格式如下:const 数据类型 常量名 = 表达式;
-
定义常量函数的格式如下:
类型说明符 函数名(参数表) const;
在对象被创建以后,其常量成员变量的值就不允许被修改,只可以读取其值。对于常量对象,只能调用常量函数。总之,常量成员变量的值不能修改,常量对象中的各个属性值均不能修改。
例如:
class CDemo {
public:
void setValue(){};//非常量成员函数
}
int main() {
const CDemo obj;//obj是常量对象
obj.setValue();//❌
return 0;
}
成员对象和封闭类
一个类的成员变量如果是另一个类的对象,则该成员变量称为成员对象
。这两个类为包含关系。包含成员对象的类焦作封闭类。
封闭类构造函数的初始化列表
当生成封闭类的对象并进行初始化时,它包含的成员对象也需要被初始化,需要调用成员对象的构造函数。在定义封闭类的构造函数时,需要添加初始化列表,指明要调用成员对象的哪个构造函数。在封闭类构造函数中添加初始化列表的格式如下:
封闭类名::构造函数名(参数表):成员变量1(参数表), 成员变量2(参数表), ... { ... }
初始化列表中的成员变量既可以是成员对象,也可以是基本数据类型的成员变量。对于成员对象,初始化列表的参数表
中列出的是成员对象构造函数的参数(它指明了该成员对象如何初始化)。
先调用成员对象的构造函数,再调用封闭类对象的构造函数。
封闭类的复制构造函数
如果封闭类的对象是用默认复制构造函数初始化的,那么它包含的成员对象也会用复制构造函数初始化。
友元
友元实际上并不是面向对象的特征,而是为了兼顾C语言程序设计的习惯与C++信息隐藏的特点,而特意增加的功能。友元的概念破坏了类的封装性和信息隐藏,但有助于数据共享,****能够提高程序执行的效率。这是一种类成员的访问权限。
友元使用关键字friend
标识。在类定义中,当friend
出现在函数说明语句的前面时,表示该函数为类的友元函数。一个函数可以同时说明为多个类的友元函数,一个类中也可以有多个友元函数。当friend
出现在类名之前时,表示该类为类的友元类。
友元函数
在定义一个类的时候,可以把一些函数(包括全局函数和其他类的成员函数)声明为友元
,这样那些函数就称为本类的友元函数。在友元函数内部可以直接访问本类对象的私有成员。在类定义中,将一个全局函数声明为本类友元函数的格式如下:
friend 返回值类型 类名::类的成员函数名(参数表);
不能把其他类的私有成员函数声明为友元函数。
友元函数不是类的成员函数,但允许访问类中的所有成员。在函数体中访问对象成员时,必须使用对象名.对象成员名
的方式。
友元函数不受类中的访问权限关键字限制,可以把它放在类的公有、私有、保护部分,结果是一样的。
//Test.hpp
#include <stdio.h>
class Pixel;
class Test {
public:
void printP(Pixel p);
void printP(Pixel *p);
};
//Test.cpp
#include "Test.hpp"
#include "Pixel.hpp"
using namespace std;
void Test::printP(Pixel p) {
cout << "test:x = " << p.x << ", y = " << p.y << endl;
}
void Test::printP(Pixel *p) {
//❌写法
//'x' is a private member of 'Pixel'
//'y' is a private member of 'Pixel'
cout << "x = " << p->x << ", y = " << p->y << endl;
}
//Pixel.hpp
#include <stdio.h>
#include <iostream>
#include "Test.hpp"
using namespace std;
class Pixel {
private:
int x, y;
public:
Pixel(int x0, int y0) {
x = x0;
y = y0;
}
void printXY() {
cout << "x = " << x << ", y = " << y << endl;
}
friend double getDist(Pixel p1, Pixel p2);
friend void Test::printP(Pixel p);
};
//main.cpp
#include <iostream>
#include <cmath>
#include "Test.hpp"
#include "Pixel.hpp"
using namespace std;
double getDist(Pixel p1, Pixel p2) {
double xDist = double(p1.x - p2.x);
double yDist = double(p1.y - p2.y);
return sqrt(xDist * xDist + yDist * yDist);
}
int main(int argc, const char * argv[]) {
Pixel p1(0, 0), p2(10, 10);
p1.printXY();//x = 0, y = 0
p2.printXY();//x = 10, y = 10
cout << "p1和p2的间距 = " << getDist(p1, p2) << endl;
//p1和p2的间距 = 14.1421
Test t;
cout << "从友元函数中输出:" << endl;
t.printP(p1);//test:x = 0, y = 0
t.printP(p2);//test:x = 10, y = 10
return 0;
}
友元类
如果将一个类B说明为另一个类A的****友元类,则类B中所有函数都是类A的友元函数,在类B的所有成员函数中都可以访问类A中的所有成员。在类定义中声明友元类的格式如下:
friend class 类名;
友元类的关系是****单向****的。若说明类B是类A的友元类,不等于类A也是类B的友元类。友元类的关系不能传递,即若类B是类A的友元类,而类C是类B的友元类,不等于类C是类A的友元类。
除非确有必要,一般不把整个类说明为友元类,而仅把类中的某些成员函数说明为友元函数。
this
指针
- C++语言规定,当调用一个成员函数时,系统自动向它传递一个隐含的参数。该参数是一个指向调用该函数的对象的指针,称为
this
指针,从而使成员函数知道对哪个对象进行操作。 - C++规定,在非静态成员函数内部可以直接使用
this
关键字,this
就代表指向该函数所作用的对象的指针。 - 在一般情况下,在不引起歧义时,可以省略
this->
,系统采用默认设置。 - 静态成员是类具有的属性,不是对象的特征,
this
表示的是隐藏的对象的指针,所以静态成员函数没有this
指针。