3. 类的特性和初始化

本文最后更新于 2024年1月3日 下午

类的特性和初始化

这是浙江大学翁恺老师的公开课,《面向对象设计C++》
视频地址:
https://www.bilibili.com/video/BV1yQ4y1A7ts/?spm_id_from=333.337.search-card.all.click&vd_source=3074f6f6ab43a114c5af8727fa4f7255

本节对应视频07-10部分。

成员变量

C语言中有本地变量(local variable)的概念,本地变量在某个函数声明中定义,且只能在这个函数中使用。C++中也有这个概念。

1
2
3
4
5
6
int TicketMachine::refundBalance() {
int amountToRefound;
amountToRefund = balance;
balance = 0;
return amountToRefund
}

比如在上面例程中,balanceTicketMachine的成员变量,而amountToRefound就是一个本地变量。
和C语言不同的是,C++的成员变量和本地变量的作用域是不同的。假如在函数中定义了一个本地变量,这个本地变量的名字和某个成员变量的名字相同,这时,C语言和C++都遵从最近原则:按照最近的定义来,因此在函数中本地变量发挥作用。

字段、参数和本地变量

所有三种类型的变量都能够存储一个与其定义的类型相适应的值。 字段(field),也就是成员变量,它定义在构造函数和方法之外。成员变量的值在对象的生成期中永远存在,只要对象存在,成员变量的值就随着对象存在。成员变量的作用域是类,在整个类的所有成员函数中可以使用这些成员变量。
参数(parameter)是指本地变量,它的值和函数有关。对本地变量而言,只能在所定义它的函数中去使用,只存在于函数内部。

假设现在有一个类A:

1
2
3
4
5
6
7
void f;
class A {
private:
int i; //这个i是一个成员变量
public:
void f();
}
.cpp文件定义这个类中的函数时:
1
2
3
4
5
void A::f()
{
int j = 10; // 这个j是一个本地变量,只能在f这个函数中才能有作用
i = 10; //i是成员变量,在所有类为A的变量中都有用
}
main函数中:
1
2
3
4
5
int main(){
A a; //此处才在主程序中真正创建了一个变量i,它是私有的,无法在主程序中访问
a.f();
return 0;
}
从上面的例子可以看出,如果类中声明了一个成员变量,每一个属于这个类的变量都存在一个这个成员变量。
参数和本地变量是相同的概念,它们的存储属性都是本地存储,只有进入到定义的函数中它们才存在。参数和本地变量都存在栈中,但是它们在栈中的存储位置是不同的。
字段是一种成员变量,它们的访问限制在整个类当中。这个类的成员函数中可以直接使用这些成员变量。

需要注意的是,定义和声明最大的不同是,定义会直接告诉编译器所定义内容的位置。声明只是告诉编译器之后会有这么一个声明的东西。

隐藏指针this

在类中需要注意的是,类中的成员变量虽然是每个定义属于这个类的变量都拥有这样一份成员变量的拷贝,但是类中声明的函数却在整个类所有的变量中都是相同的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class A{
public:
int i;
void f();
};

void A::f(){
i = 20;
cout << i << endl;
}

int main(){
A a;
a.i = 10;
cout << a.i; // 此时i的值是10
a.f(); // 此时i的值会更改为20
cout << a.i;
};

如果此时再有一个类A的变量aaA aa,那么函数f如何知道处理的iai还是aai? 这件事情可以利用指针做到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class A{
public:
int i;
void f();
};

void A::f(A* p){
p -> i = 20;
cout << p -> i << endl;
}

int main(){
A a;
A aa;
a.i = 10;
cout << a.i; // 此时i的值是10
a.f(); // 此时i的值会更改为20
f(&a);
cout << a.i;
aa.i = 100;
aa.f();
};
C++中用不同的对象调用同一个函数的时候,这个函数是可以知道是哪一个类/哪一个变量在调用它的。C++里所有的成员函数都有一个隐藏参数this
1
void Point::print()
在C++中同等于:
1
void Point:print(Point *p) //Point *p 就是 this
this是一个属于类中所有成员函数都具有的本地变量,在C++中是一个关键字,可以被直接使用,比如this->i=20.

对象的初始化·构造和析构

初始化对象

在使用对象之前,需要对其中的变量进行初始化。
比如下面的代码中:

1
2
3
4
5
6
7
8
9
10
class Point {
public:
void print() const;
void move(int dx, int dy)
private:
int x;
int y;
};

Point a;
可以发现,在定义一个变量a属于类Point时,其中的private变量xy都只是被声明过,但是值没有进行初始化定义。
C++中并没有要求在创建一个属于某个类的变量的时候,这个变量内部的成员变量的内存应该被归零。比如现在创建的属于类Point的变量a其中的成员变量xy的值事实上是不确定的,此时xy对应的内存内容有可能为0,也有可能原本就有其他内容。

Visual Studio的Debug模式会在编译程序的时候往对象的内容中填充0xcd(这是一种称为足迹调试的方法),用于表明这个对象(中的变量)没有被初始化过。两个0xcd正好对应国标码的汉字。当程序的编译结果出现“烫”的时候,表明程序遇到了没有初始化的内存。

因此有必要对成员变量进行初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13

class Point {
public:
void init(int x, int y); // 增加对x和y的初始化函数
void print() const;
void move(int dx, int dy)
private:
int x;
int y;
};

Point a;
a.init(1,2); //初始化x和y的值

构造函数和析构函数的概念

构造函数

构造函数(constructor)是一种特殊的成员函数,它的名字和类的名字相同,没有返回类型:

1
2
3
4
5
class X {
int i;
public:
X();
}
构造函数可以在对象被创建时自动初始化该对象的成员变量。构造函数的目的是保证对象在创建时能够被正确初始化,避免出现未初始化的内存问题。
只要使用属于这个类的变量,构造函数就会被自动调用。
在定义对象时构造函数会被自动调用,可以有参数:
1
2
Tree(int i){...};
Tree(23);
构造函数的作用域和生命周期和变量相同,都存在于对象当中。

析构函数

当对象变量需要被消除的时候,就会调用析构函数(destructor),用于释放这个对象所占用的内存,以~表示:

1
2
3
4
class Y{
public:
~Y();
};
析构函数没有返回,也不能有参数。析构函数会在对象变量的作用域(scope,一个{}中的内容就是一个作用域)结束的时候被自动调用。

构造函数和析构函数的使用

编译器会在{}开始时,也就是作用域开始的时候为作用域中所有的变量分配内存,但是构造函数的调用要直到类的变量被定义时才会发生。

1
2
3
4
int main(){ //此时编译器为内部所有的变量分配内存
A a; // a的构造函数被调用
a.i=10;
} // a的析构函数被调用

来看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
void f(int i) {
if(i < 10) {
goto jump1;
}
X x1; // x1的构造函数本来应该在这里被调用,但是因为goto跳过了x1的初始化,x1的构造函数未被调用
jump1:
switch(i){
case 1:
X x2;
break;
}
} // x1,x2的析构函数被调用,由于x1的构造函数没有被调用,因此编译会出错
当构造函数未被调用时,对应的析构函数也无法被调用。

程序中其他组成的初始化

变量的初始化

C++和C语言一样,变量可以在函数的任何地方进行定义,但是使用之前必须要经过初始化。上面的内容也提到,编译器会在作用域开始的时候为作用域内的所有变量分配内存。

数组的初始化

数组的初始化和C语言相同:

1
2
3
4
int a[5] = {1,2,3,4,5};
int b[6] = {5};
int c[] = {1,2,3,4}; //没有限定C中的元素个数
sizeof c / sizeof *c //计算数组C当中的元素个数

结构体的初始化

也可以用类似的方法来初始化结构体:

1
2
struct X { int i; float f; char c;};
X x1 = {1,2.2, 'c'}
在C++中,也可以通过结构体去定义类(结构体允许存在函数):
1
struct Y {float f; int i; Y(int a);}; //最后一项是Y的构造函数
有了构造函数之后,就需要调用构造函数来初始化这个类/结构体:
1
Y y1[] = {Y(1),Y(2),Y(3)};

默认构造函数

没有参数的构造函数叫做默认构造函数(default constructor),不代表编译器会自动提供。

1
2
Y y1[] = {Y(1),Y(2),Y(3)};
Y y2[2] = {Y(1)}; //这一行编译不会通过,因为编译器无法自动提供一个构造函数来初始化第二个元素
报错信息如下:
1
>> no matching function for call to 'Y:Y()'


3. 类的特性和初始化
https://l61012345.top/2023/12/11/学习笔记/C++面向对象设计/3. 成员变量/
作者
Oreki Kigiha
发布于
2023年12月11日
更新于
2024年1月3日
许可协议