当前位置: 首页 > news >正文

06【C++ 初阶】类和对象(上篇) --- 初步理解/使用类

文章目录

  • 全文总结
  • 前言
  • 初识面向对象
    • 1. “自定义”一个类
      • 1.1 封装
      • 1.2 访问限定符
        • public、private:
      • 1.3 作用域
      • 1.4 类成员函数的声明定义分离
        • 类的两种定义方式:
    • 2. 实例化对象
      • 2.1 类的大小计算
        • 2.1.2 类对象成员的存储方式
          • 类对象为什么不采用第2种方式而采用第3种方式存储
        • 2.1.3 当多成员变量时的内存大小计算(内存对齐)
    • 3.this指针
      • 3.1 this指针的特性

全文总结

  1. 面向对象是一种思想,将事情划分为一些对象,贴近现实,降低耦合。​用有明确责任、边界清晰、相互协作的“整体”(对象)来建模系统,代替零散的全局数据和函数调用。​
  2. 类就是C++面向对象的产物,它是C++中的类型,只不过交给我们去自定义。
  3. 实现面向对象,就需要做到:封装和抽象,类的封装是实现面向对象的基石;类的抽象就是公开接口,隐藏细节 — 抽象成一个整体,而不是零散的过程。
  4. 为了实现封装,C++的类中也可以定义成员函数。
  5. 为了实现抽象,C++给出了三个访问限定符,让工程师自己决定这个类哪些东西可以被访问,哪些东西不行,从而降低耦合度。
  6. 类的封装和抽象本质上还是一种管理,让使用者不必关注类内部实现细节,只需要知道怎么使用。
  7. 类也有自己的类域,虽然不会报错,我们为了可读性,还是尽量将类中的变量用“_”区分。
  8. 类的成员函数也可以声明定义分离,如果直接定义在类中的成员函数,默认是内联的。
  9. 平常我们对一个类型的定义,其实是在声明它内部的成员变量和成员函数;若成员函数在类内实现(有函数体),则同时完成了该成员函数的定义。
  10. 当我们真正使用类型去实例化成一个对象的时候,它的成员变量才被定义。
  11. 类对象采用保存成员变量,但是成员函数存放在公共代码区的存储方式。
    因为这种方式的函数调用其实是像我们正常的函数调用一样的,它可以保持最低的访存次数。
  12. 我们的类的成员变量的存储方式,需要内存对齐,这样以空间换取了内存的访问速度。
  13. 我们的成员函数,会被编译器自动的加上一个形参(this指针),为的是在调用函数时让函数修改正确的对象。
  14. 普通函数的调用处,最终都是一个Call地址跳转而已,我们函数的参数,是由函数的调用方存入寄存器或者是栈,在CPU执行到我们的“函数体”的时候,我们的“函数体”直接操作寄存器或者栈中的数据即可,这就是传参的本质。

前言

通过00【C++ 入门基础】前言得知,C++是为了解决C语言在面对大型项目的局限而诞生:

C语言面对的现实工程问题(复杂性、可维护性、可扩展性、安全性)

C语言面临的具体问题:

1.struct 数据公开暴露,函数数据分离,逻辑碎片化。(复杂性、安全性)
2.修改数据结构,如 struct 新增字段,可能导致大量相关函数需要修改。(可维护性)
3.添加新功能常需修改现有函数或结构体,易引入错误。(可扩展性)
4.资源(内存、文件句柄)需手动管理,易泄漏或重复释放。(安全性)

C++的类,解决了上述的所有问题,而本文《类(上篇)》,所讲的是类如何解决上述第1点和第2点问题。


初识面向对象

面向对象OOP,顾名思义就是关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。(C语言是面向过程的)

面向过程: 主要关注整个过程的完成流程,每个过程之间关联度强。在这里插入图片描述

一个对象的属性,分为数据属性和行为属性,即它的特征和它能做什么。

面向对象:将洗衣服这个事情拆分成多个对象,每个对象有它各自的属性,比如人有手搓衣服的行为属性,有分男人女人的数据属性,衣服有干净与否的数据属性,也有被洗、被拧干的行为属性。在这里插入图片描述

这样每个对象之间的耦合度就会非常低,互相之间就互不影响。

因为C语言是面向对象的,所以如果我们要去修改其中一个过程,那么就会变得非常的困难,比如放衣服的过程,我要给放衣服的函数加一个参数,意义为放多少件衣服,那么后续的函数就都需要一起修改,来得知我前面放了多少件衣服。
而采用面向对象的方法,衣服对象里面可以带一个数据属性参数表示衣服的件数,那么我们其他的类要想得知有多少件衣服,就只需要指定的访问衣服对象的属性即可。

类,就是C++实现面向对象的产物。


类其实就是C++中的类型(像int一样),只不过是交给我们去自定义的,我们将一个现实对象的所有属性(数据和行为)抽离出来,放到这个我们自定义的类型中去,最后这个类型实例化出的变量,就是一个程序中的对象!

比如,我们抽离出现实中桌子这个对象的所有属性 — 数据属性(长、宽、高、材质、坚硬程度、是否有物体放在我的上面)/ 行为属性(可以放东西,可以被举起来移动)等等,将所有的属性都用C++代码的方式,放到我们的自定义类型Table中,那么最后我们用这个类实例化出的一个变量table001,就是我们程序世界中的一个“桌子”对象。

其实在我们抽离现实对象的属性,并让我们自定义类型,和它最终实例化出的对象更加的贴近现实的过程中,我们已经做到了:封装,即将一个对象的数据属性和行为属性封装在一起;
但是要让我们的类型更像现实,耦合性更低,我们的类型还需要做到:抽象,就是开放可以被别人访问的属性,而隐藏属性具体的实现细节。

比如,我们程序中的对象桌子,不需要知道其长宽高(数据属性),就可以在它的上面放东西(行为属性)。

实现面向对象的基石:
封装,即将所有的事物的行为属性、数据属性集成在一起称为一个“类”;
抽象,即将“类”的行为属性(接口)公开,数据属性(细节)隐藏 — 抽象成一个整体,而不是零散的过程。

接下来来看,C++如何实现封装和抽象的。


1. “自定义”一个类

C++中“定义”类,使用class关键字:

class className
{// 类体:由成员函数和成员变量组成int length;int width;int height;
};
int main()
{className ch1;return 0;
}

class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。
而且,结构体struct,在C++中,也被升级为了类:

struct Table
{int length;int width;int height;
};int main()
{struct Table Ta1;  //C语言的写法Table Ta2;         //C++的写法return 0;
}

结构体也保留了C语言的写法,为了向前兼容。

它们之间的区别,看下面的讲解

1.1 封装

C语言结构体中只能定义变量,在C++中,类内不仅可以定义变量,也可以定义函数,也就是将我们的一个现实事物的数据属性和行为属性放在一起,这就是封装

#include<iostream>
using namespace std;
class Chair
{void init(int L, int W, int H){length = L;width = W;height = H;}void show_information(){cout << "长:" << length << " 宽:" << width << " 高:" << height;}int length;int width;int height;
};struct Table       //C++中struct升级为类,所以它也可以做封装操作.
{void init(int L, int W, int H){length = L;width = W;height = H;}void show_information(){cout << "长:" << length << " 宽:" << width << " 高:" << height;}int length;int width;int height;
};

将一个事物的的行为属性也封装进类中。

类体中内容称为类的成员:
类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数。

那么我们访问成员(成员变量、成员方法)的方式,也和我们C语言时访问成员一样:

int main()
{Table Ta;Ta.init(10, 5, 1);Ta.show_information();      //最后打印:   "长:10 宽:5 高:1"Ta.length = 52013140;Ta.show_information();      //最后打印:   "长:52013140 宽:5 高:1"return 0;
}
int main()
{Chair Ch;Ch.init(10, 5, 1); //报错:“Chair::init”: 无法访问 private 成员(在“Chair”类中声明)Ch.show_information(); //报错:“Chair::show_information”: 无法访问 private 成员(在“Chair”类中声明)return 0;
}

那么我们这里遇到两个问题:

  1. 为什么我们的struct定义的变量的时候可以访问,而我们class定义的反而不可以访问了呢?
  2. 我们既然上面的Ta变量可以直接访问它的成员变量,那不是别人想改就改吗?要是改成起奇奇怪怪的值怎么办?

原因就在于,我们C++为了让我们的类更贴近现实,更低耦合,它在封装的基础上,还实现了抽象,看下面的解释。

1.2 访问限定符

抽象,即将“类”的行为属性(接口)公开,数据属性(细节)隐藏 。

为什么说我们的抽象,可以让类更贴近现实,耦合更低呢?
因为在现实生活中,事物之间都是只能看到表面,所以彼此之间有很强的独立性,正如:
我可以和你聊天(开放接口),但是你永远触及不到我内心真正的想法(封装内心),
所以人和人之间感情才如此淡,所以人和人之间的关联度才如此低,所以人和人之间的耦合性才如此低。

C++对于类的做法,也是一样的,它规定了特定的关键字给工程师使用,可以让工程师自己决定,这个类的成员中,哪些东西是可以开放给别人的,哪些东西是需要封存在内心深处的,无法被被人触及的,这样,不同类、变量之间的耦合度就降低了。

三个访问限定符:
在这里插入图片描述
我们这里先认为,protected和private是一样的,不做区分所以就先不说protected。

public、private:

我们的这两个访问限定符,都是在类中使用的关键字,正如名字一样,public修饰的,是可以被类外访问的,private修饰的,是不可以被类外访问的,只能在类的内部使用。

我们上面的class定义的Chair Ch;变量为什么不能访问它的成员,原因就在于我们的class定义的类,它的所有成员默认就是用private修饰的;而我们struct可以访问,正因为它的所有成员默认是用public修饰的。

#include<iostream>
using namespace std;
class Chair
{
public:         //我们将类中的两个成员函数函数用public公开给类外,那么类外就可以访问了.void init(int L, int W, int H){length = L;width = W;height = H;}void show_information(){cout << "长:" << length << " 宽:" << width << " 高:" << height;}
private:int length;int width;int height;
};struct Table       //C++中struct升级为类,所以它也可以做封装操作.
{
public:void init(int L, int W, int H){length = L;width = W;height = H;}void show_information(){cout << "长:" << length << " 宽:" << width << " 高:" << height;}
private:           //我们将类中的三个成员变量用private私有,那么类外就访问不了了.int length;int width;int height;
};
int main()
{Table Ta;Ta.init(10, 5, 1);Ta.show_information();      //最后打印:   "长:10 宽:5 高:1"//Ta.length = 250;            //报错: “Table::length”: 无法访问 private 成员(在“Table”类中声明)Chair Ch;Ch.init(10, 5, 1);Ch.show_information();      //最后打印:   "长:10 宽:5 高:1"return 0;
}

这样我们就可以正常的访问成员变量,完美的解决了上面的两个问题:

  1. 为什么我们的struct定义的变量的时候可以访问,而我们class定义的反而不可以访问了呢?
  2. 我们既然上面的Ta变量可以直接访问它的成员变量,那不是别人想改就改吗?要是改成起奇奇怪怪的值怎么办?

封装和抽象不仅是一种对于耦合度的考虑,还让使用者更加方便:
封装本质上是一种管理,让用户更方便使用类。比如:对于电脑这样一个复杂的设备,提供给用户的就只有开关机键、通过键盘输入,显示器,USB插孔等,让用户和计算机进行交互,完成日常事务。但实际上电脑真正工作的却是CPU、显卡、内存等一些硬件元件。

对于计算机使用者而言,不用关心内部核心部件,比如主板上线路是如何布局的,CPU内部是如何设计的,用户只需要知道,怎么开机、怎么通过键盘和鼠标与计算机进行交互即可。因此计算机厂商在出厂时,在外部套上壳子,将内部实现细节隐藏起来,仅仅对外提供开关机、鼠标以及键盘插孔等,让用户可以与计算机进行交互即可。
类的封装,亦是如此。

1.3 作用域

在 01【C++ 入门基础】命名空间/域中我们知道,域(Scope)​​ 是一个核心概念,它定义了程序中标识符(如变量、函数、类名)的可见性与生命周期范围。

而类也有自己的类域,当我们定义了一个新的类,类体{ … } 内的所有声明都属于这个类的作用域。

class MyClass {// 从这里开始,进入 MyClass 的类域int x;                // 在 MyClass 域中声明的成员变量void func();          // 在 MyClass 域中声明的成员函数class NestedClass {}; // 在 MyClass 域中声明的嵌套类
}; // MyClass 的类域结束

成员变量命名规则的建议:

class Date
{
public:void Init(int year){// 这里的year到底是成员变量,还是函数形参?year = year;}
private:int year;
};

如果我们类中成员函数的形参和内部成员变量是一样的,那么对于不清楚的使用这来说,就很难区分了,虽然它不是错的。
在这里插入图片描述

// 所以一般都建议这样
class Date
{
public:void Init(int year){_year = year;}
private:int _year;
};

将类内的成员,加上一个下划线的前缀,或者其他的区分方式都可以。

1.4 类成员函数的声明定义分离

前面我们知道了,类要做封装,所以,一个类的成员函数和成员变量,是需要放在一起,同属于一个类的,但是其实我们类的成员函数,也是可以声明定义分离的。

类的两种定义方式:
  1. 声明和定义全部放在类体中。
    在这里插入图片描述
  2. 类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名和域作用限定符::,指定这个函数是属于该类的成员。
    在这里插入图片描述
    两种方式都可以,但是建议使用第二种。

那么如果我想让一个成员函成为内联,我可以声明定义分离,并在类中的声明处使用inline修饰它吗?
不可以,在05【C++ 入门基础】内联、auto、指针空值中我们知道,内联不可以声明定义分离,如果想让一个类的成员函数是内联,那么只能直接将它定义在类中,另外,直接定义在类中的成员函数,本身默认内联。


2. 实例化对象

对于我们函数来说,声明和定义的区别是什么呢?

声明只有函数名、参数、返回值,没有函数体。

对于变量呢?

是有没有开空间,如果我们指定一个变量,但是它实际上没有开空间,那么我们一般就说它是对一个变量的声明!就像我们extern int A; 这就是声明了一个外部变量。

所以在我们定义类时,成员变量,它是没有开辟空间的,所以定义类的时候,对于内部的成员变量来说,是一种声明,
只有当我们用类创造出一个类的变量,开辟了这个类的变量空间时,类中的成员变量才真正的被定义!

使用我们定义的类,去创造变量的过程,我们称为类实例化成对象,或者称为类的对象的定义。

对象: 用自定义类去定义出的变量。
实例化:用自定义类去定义出变量的这个过程。

注意,定义类和定义类对象,有本质区别:

语句标准术语实体类别内存影响
class Point { … };​类定义类型无直接分配
Point p;对象定义对象分配存储空间
extern Point p;​对象声明对象(引用)无分配
class Point;类声明​类型(引用)无分配

所以其实我们平时定义类(定义类型)的过程就是:

同时完成类中成员变量的声明和成员函数的声明;若成员函数在类内实现(有函数体),则同时完成了该成员函数的定义。

做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间。
在这里插入图片描述
在这里插入图片描述

2.1 类的大小计算

问题:类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算一个类的大小?

2.1.2 类对象成员的存储方式

我们下面来猜测一下,类对象的成员变量,到底是存在哪里的:

  • 1.对象中包含类的各个成员:
    在这里插入图片描述

缺陷:每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份代码,相同代码保存多次,浪费空间。那么如何解决呢?

  • 2.代码只保存一份,在对象中保存存放代码的地址:
    在这里插入图片描述
    这种方式好像可行?

  • 3.只保存成员变量,成员函数存放在公共的代码段:

在这里插入图片描述
那么对于我们的第2和第3种方法,到底是使用哪一种呢?我们来验证一下:

// 类中既有成员变量,又有成员函数
class A1 {
public:void f1() {}
private:int _a;
};// 类中仅有成员函数
class A2 {
public:void f2() {}
};// 类中什么都没有---空类
class A3
{
};int main()
{cout << sizeof(A1) << "   " << sizeof(A2) << "   " << sizeof(A3); //输出: 4   1   1return 0;
}

我们可以看到,除了成员变量,没有其他东西参与了类对象的大小计算,所以可以推断:

  1. 我们类对象的存储方式是第三种,即对象中只保存成员变量,成员函数保存在公共代码区。
  2. 注意空类的大小,空类比较特殊,虽然没有成员,编译器给了空类一个字节来唯一标识这个类的对象。
类对象为什么不采用第2种方式而采用第3种方式存储

第二种方式,采用的是类似一个函数指针表的方式存储类的成员,
而第三种方式,是像我们普通的函数一样,最后在对应有调用的地方展开函数的地址,
对比两种函数的设计方式:

  1. 第3种方法,类对象调用成员函数通过直接绑定函数地址:
//当我们采用类成员函数放在公共代码区的方式,最后的成员函数其实是在有调用的地方展开了该函数地址。
//因为这种方式,我们函数的最终存储方式是在代码段,大概率会和执行的指令一起进入CPU缓存,所以大概率不会有访存。
//这种方式和我们普通的函数调用一一样,是通过汇编阶段生成函数的符号,然后在最终调用的地方展开函数的地址。
call 0x401230  ; 目标地址硬编码
  1. 第2种方法,类对象调用成员函数,需要先找到类的函数指针表:
//这种访问方式,我们函数
//我们需要先找到这个对象的地址,因为对象内部存储了它的成员函数指针表(第一次访存),
//然后找到成员函数指针表的地址(第二次访存),
//通过函数指针表,间接的访问对应的函数(第三次访存)。
mov rax, [obj]      ; 指令1:取对象地址 → 访问内存
mov rbx, [rax+8]    ; 指令2:取函数表指针 → 访问内存
call [rbx+0x10]     ; 指令3:间接调用 → 访问内存

从访问内存的次数来说,无疑是我们直接绑定的方式,效率更加的高效。

2.1.3 当多成员变量时的内存大小计算(内存对齐)

内存对齐规则:

  1. 第一个成员在与结构体偏移量为0的地址处。

  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。

  3. 对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。 VS中默认的对齐数为8

  4. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。

  5. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
    在这里插入图片描述

系统默认对齐数8
double a是8字节的,8 == 8,所以变量a的对齐数是8,由于它是类中的第一个变量,所以它放在偏移量为0的位置;
char b是1字节的,1 < 8,所以变量b的对齐数是1,它应该放在偏移量为1的倍数的地址处,8是1的倍数,所以放在8偏移量地址处;
int c是4字节的,4 < 8,所以变量c的对齐数是4,它应该偏移量为4的倍数的地址处,12是4的倍数,所以放在12偏移量地址处。
我们成员变量中最大的对齐数是8,8与系统默认对齐数取最小值,得8,所以整个S1类的大小应该为8的倍数,我们上图占用了16个字节,是8的倍数,无需补充,所以我们该类的大小是16字节。

如果不内存对齐,会怎么样?
CPU 从内存中读取数据通常是按固定大小的块进行的(称为内存总线宽度或字长,如 4 字节、8 字节)。例如,一个典型的 64 位 CPU 更倾向于一次读取 8 个字节的数据。这也是为什么我们的默认的对齐数是8或者4。
我们假设系统默认对齐是4

  1. 对齐的情况:
    在这里插入图片描述

  2. 未内存对齐的情况:
    在这里插入图片描述

若不采用内存对齐,我们CPU要获取变量_a,需要读取两次,先读取第一个块的一部分,再读取第二个块的一部分,然后把两个块中的相关部分组合起来才能得到。
这比只需一次读取操作(对齐时)要 ​慢得多

默认对齐数也是可以改的,它并不影响机器的读取,只是改变了存储方式,
内存对齐,其实是一种以空间换时间的方法。


3.this指针

我们知道了我们的类的成员函数是放在公共代码区,供该类的所有对象调用的,那么有一个问题:
我们一个类可以实例化成多个对象,这多个对象,调用的又都是相同的成员函数(成员函数中有修改对象自己的成员变量的逻辑),那么我们的成员函数,怎么知道是哪个对象调用它呢?
它如何保证不会改成这个类的其他对象呢?

定义一个日期类做演示:

class Date
{
public:void Init(int year, int month, int day){_year = year;_month = month;_day = day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}
private:int _year; // 年int _month; // 月int _day; // 日int a;
};
int main()
{Date d1, d2;d1.Init(2022, 1, 11);d2.Init(2022, 1, 12);d1.Print();d2.Print();return 0;
}

Date类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调用 Init 函数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?

C++中通过引入this指针解决该问题,即:C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,
让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。
只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。

说人话就是:
编译器会给类中每个非静态的成员函数,自动加上一个参数,这个参数是该类的指针(this指针),并在成员函数被调用的地方,会自动将调用该函数的对象的地址传给这个this指针,那么成员函数就可以通过这个this指针,知道是哪个对象,从而不会修改错。

3.1 this指针的特性

  1. this指针的类型:类类型* const,即成员函数中,不能给this指针赋值。
  2. 只能在“成员函数”的内部使用
  3. this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
  4. this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递

在这里插入图片描述


本文章为作者的笔记和心得记录,顺便进行知识分享,有任何错误请评论指点:)。

http://www.lryc.cn/news/588557.html

相关文章:

  • ThreadLocal内部结构深度解析
  • 《大数据技术原理与应用》实验报告三 熟悉HBase常用操作
  • 每天一个前端小知识 Day 31 - 前端国际化(i18n)与本地化(l10n)实战方案
  • html js express 连接数据库mysql
  • Java:继承和多态(必会知识点整理)
  • 为什么资深C++开发者大部分选vector?揭秘背后的硬核性能真相!
  • 9.服务容错:构建高可用微服务的核心防御
  • #Paper Reading# Apple Intelligence Foundation Language Models
  • 微服务初步入门
  • 量子计算新突破!阿里“太章3.0”实现512量子比特模拟(2025中国量子算力巅峰)
  • 【算法训练营Day12】二叉树part2
  • 《大数据技术原理与应用》实验报告二 熟悉常用的HDFS操作
  • 【小白量化智能体】应用5:编写通达信股票交易指标及生成QMT自动交易Python策略程序
  • UDP协议的端口161怎么检测连通性
  • 【PY32】如何使用 J-Link 和 MDK 开发调试 PY32 MCU
  • 【STM32】什么在使能寄存器或外设之前必须先打开时钟?
  • java基础-1 : 运算符
  • 使用dify生成测试用例
  • 13.计算 Python 字符串的字节大小
  • HTML 文本格式化标签
  • 工业新引擎:预测性维护在工业场景中的实战应用(流程制造业为例)
  • 具身智能零碎知识点(五):VAE中对使用KL散度的理解
  • JJ20 Final Lap演唱会纪念票根生成工具
  • HashMap的长度为什么要是2的n次幂以及HashMap的继承关系(元码解析)
  • C语言:20250714笔记
  • 文本预处理(四)
  • AI驱动编程范式革命:传统开发与智能开发的全维度对比分析
  • 【DataWhale】快乐学习大模型 | 202507,Task01笔记
  • js的局部变量和全局变量
  • Java面试总结(经典题)(Java多线程)(一)