类是数据类型,对象是内存的具体分配

本文介绍了C#中面向对象的基本概念,包括类和对象的定义、实例化过程以及如何比较对象。通过创建扑克牌类的例子,详细解释了类的属性和方法。

对于初学面向对象的人来说,往往很疑惑什么是对象,什么是类。在面向对象的编程领域中,我们需要明确一个事实:所有的类型必须被明确的表示,而不是被其他数据类型来替代。

类是 C# 中功能最为强大的数据类型,它定义了数据类型的数据和行为。然后,程序员可以创建作为此类的实例的对象。类支持继承,而继承是面向对象编程的基础部分。

比如我们在前面了解的一个关于洗牌的过程,在前面的代码中,我们计算和产生的不是真正意义上的扑克牌,而是字符串!

如果要真正的描述扑克牌,我们必须要有一个扑克牌的数据类型,而这个类型.NET Framework显然没有提供,因此我们需要自己编写一个扑克牌的类型。所以我们需要定义一个扑克牌的类型,我们把自己定义的类型一般称为类。

/// <summary>

/// 扑克牌

/// </summary>

public class Poker

{

/// <summary>

/// 扑克牌的花色

/// </summary>

public string Style;

/// <summary>

/// 扑克排的牌面

/// </summary>

public string Title;

/// <summary>

/// 扑克牌的值

/// </summary>

public int Value;

}

上述代码描述我们使用关键字class定义了一个类,该类的名字叫Poker。这个类中定义了3个变量:Style、Title和Value。

class 关键字前面是访问级别。由于在该例中使用 public,因此任何人都可以基于该类创建对象。类的名称位于 class 关键字的后面。定义的其余部分是类的主体,用于定义行为和数据。类的字段、属性、方法和事件统称为“类成员”。

要使用这个类,我们需要对其实例化。所谓的实例化,说白了就是通知编译器到内存中开辟一个空间,以存储一些值。通过使用 new 关键字(后跟对象将基于的类的名称)可以创建对象:

Poker pokerA = new Poker(); //实例化Poker类

Poker pokerK = new Poker(); //实例化Poker类

创建类的实例后,将向程序员传递回对该对象的引用。在前面的示例中,pokerA和pokerK是对基于 Poker的对象的引用。此引用引用新对象,但不包含对象数据本身。

static void Main(string[] args)

{

Poker pokerA = new Poker(); //实例化Poker类

pokerA.Style = ((char)4).ToString();

pokerA.Title = "A";

pokerA.Value = 1;

System.Console.WriteLine("{0}{1}", pokerA.Style, pokerA.Title);

Poker pokerK = new Poker(); //实例化Poker类

pokerK.Style = ((char)4).ToString();

pokerK.Title = "K";

pokerK.Value = 13;

System.Console.WriteLine("{0}{1}", pokerK.Style, pokerK.Title);

}

我们得到的结果如图3.3.1:

clip_image002

图3.3.1

Poker pokerA = new Poker()和Poker pokerK = new Poker()代码用以产生一个实例化Poker对象。通俗的说就是,通知编译器,我需要一个新的空间,这个空间请按Poker类的定义来分配空间,并且命名为pokerA或pokerK。而Style、Title和Value是Poker类的值,存储了Poker具体的内容。

如果要比较两张牌对象有两种方案:

一、比较两张牌对象是不是在同一个内存空间,如果是的话,这两个对象完全相等。

二、比较两张牌对象的值是不是各自相等,如果是的话,表示这两张牌的值或牌面一样。

static void Main(string[] args)

{

Poker pokerA1 = new Poker(); //实例化Poker类

pokerA1.Style = ((char)4).ToString();

pokerA1.Title = "A";

pokerA1.Value = 1;

System.Console.WriteLine("{0}{1}", pokerA1.Style, pokerA1.Title);

Poker pokerA2 = new Poker(); //实例化Poker类

pokerA2.Style = ((char)4).ToString();

pokerA2.Title = "A";

pokerA2.Value = 1;

System.Console.WriteLine("{0}{1}", pokerA2.Style, pokerA2.Title);

System.Console.WriteLine(pokerA1 == pokerA2); //在两个内存空间中,两个对象不相等

System.Console.WriteLine(pokerA1.Value == pokerA2.Value);//两张牌的值相等

}

运行的结果如图3.3.2:

clip_image004

图3.3.2

初学者很不了解对象的相等和对象的值相等是什么意思,用一个通俗的场景来说,有两个双胞胎兄弟,长的是一模一样,它们的衣服值一样,它们的外观值一样,它们的所有的一切的值都一样,但它们就是两个人,而不能说是同一个人。

而笔者名字叫“王豫翔”,在QQ的昵称上叫“炽天使”,MSN叫“害羞的狮子王”,但其实都是同一个人。

以下代码描述了两个对象相等,初学者要牢记,如果两个对象相等,任意的对象改变的值,会影响另一个对象的值,因为它们使用的是共同的内存。

static void Main(string[] args)

{

Poker pokerA1 = new Poker(); //实例化Poker类

pokerA1.Style = ((char)4).ToString();

pokerA1.Title = "A";

pokerA1.Value = 1;

System.Console.WriteLine("{0}{1}", pokerA1.Style, pokerA1.Title);

Poker pokerA2 = new Poker(); //实例化Poker类

pokerA2 = pokerA1; //将pokerA1的内存空间的地址赋值给pokerA2

System.Console.WriteLine(pokerA1 == pokerA2);

System.Console.WriteLine(pokerA1.Value == pokerA2.Value);

pokerA2.Style = ((char)5).ToString(); //pokerA2改变了花色

System.Console.WriteLine("{0}{1}", pokerA1.Style, pokerA1.Title); //pokerA1的花色值也改变了

}

观察图3.3.3结果,并理解对象的引用关系。

clip_image006

图3.3.3

初学者注意

出现上述原因是应为C#的数据类型分为:值类型和引用类型。对象是引用类型。具体的细节在后面我们将逐步介绍,稍安毋躁。

<think>好的,我现在需要回答用户关于栈对象是否在编译时分配内存并初始化的问题。首先,我要回忆一下栈对象的基本概念和内存分配机制。栈对象通常指的是在函数作用域内声明的局部变量,它们的内存管理是通过栈来实现的。 用户的问题集中在编译时是否就已经分配内存和进行初始化。根据之前的学习,编译阶段主要是将源代码转换为机器码,处理语法和生成目标文件,而内存分配通常发生在程序运行时。不过,可能存在一些特殊情况,比如静态存储期的变量,但栈对象属于自动存储期,应该是在运行时处理的。 接下来,我需要明确内存分配的时间点。栈对象内存在函数被调用时分配具体来说,当函数被调用时,栈帧会被创建,此时栈对象内存空间才会被分配。因此,编译时只是确定变量的大小和位置,但实际的内存分配是在运行时函数执行的时候进行的。 关于初始化,栈对象的初始化也是在运行时发生的。例如,在函数内部声明一个int变量并赋初值,这个赋值操作是在程序执行到该声明语句时进行的。如果变量没有显式初始化,其值可能是随机的,这进一步说明初始化发生在运行时。 用户可能混淆了编译时和运行时的区别,或者将静态变量与栈对象混为一谈。需要指出的是,静态变量(如全局变量或static局部变量)的初始化可能在程序启动时或首次使用时进行,但栈对象不同。 另外,考虑到不同的编程语言可能有不同的行为,但用户提到的是“栈对象”,通常与C++或似语言相关,因此应基于这些语言的机制来解释。例如,在C++中,局部对象的构造函数在进入作用域时被调用,这也是运行时行为。 需要验证是否有例外情况,比如编译器优化可能导致某些栈对象在编译时被优化掉,但这属于特殊情况,不影响一般情况下的结论。因此,栈对象内存分配和初始化主要发生在运行时,而非编译时。 总结起来,用户的疑问可能源于对编译和运行过程的不完全理解,需要通过具体例子和分步骤解释来澄清。同时,强调编译时处理的是代码结构和符号,而运行时处理内存管理和执行流程。</think>--- ### **栈对象内存分配与初始化的深度解析** --- #### **一、核心结论** **栈对象内存分配与初始化发生在运行时,而非编译时** 栈对象(即局部自动变量)的内存管理完全由程序的运行时栈控制,编译阶段仅进行**符号记录与空间计算**,实际内存操作在函数执行时动态完成。 --- #### **二、分阶段详解** ##### **1. 编译阶段:准备蓝图** - **符号表记录**:编译器识别变量名、型、作用域,计算所需栈空间大小 ```cpp void func() { int a; // 记录a为int型,占4字节 double b[5]; // 记录b为double[5],占40字节(假设double=8字节) } ``` - **生成指令模板**:创建函数入口/出口的栈帧调整指令(如x86的`push ebp; mov ebp, esp; sub esp, N`) ##### **2. 链接阶段:地址无关计算** - **确定相对偏移量**:计算变量在栈帧内的相对位置(如`a`位于`ebp-4`,`b`位于`ebp-44`) ##### **3. 运行时阶段:动态操作** - **内存分配**:函数被调用时,通过调整栈指针(ESP)一次性分配整个栈帧 ```nasm ; x86汇编示例 func: push ebp ; 保存旧栈帧基址 mov ebp, esp ; 新栈帧基址 sub esp, 48 ; 分配48字节栈空间(4+40+对齐填充) ; 此时a的地址为[ebp-4], b为[ebp-44] ``` - **初始化时机**: - **内置型**:未显式初始化时值为随机数据(内存残留) ```cpp int x; // 未初始化,值随机 int y = 10; // 初始化在运行时赋值 ``` - **型**:执行构造函数 ```cpp class Demo { public: Demo() { /* 构造代码在运行时执行 */ } }; void test() { Demo d; // 运行时调用构造函数 } ``` --- #### **三、关键验证实验** ##### **实验1:观察未初始化值** ```cpp #include <iostream> void check() { int uninit; std::cout << "未初始化值: " << uninit << std::endl; // 每次运行输出不同 } int main() { check(); // 输出随机值(如-272632456) check(); // 输出另一个随机值(如32764) } ``` **结论**:栈空间在分配时未清零,证明内存分配是运行时行为。 ##### **实验2:构造顺序验证** ```cpp #include <iostream> class Trace { public: Trace() { std::cout << "构造函数执行于运行时" << std::endl; } }; int main() { std::cout << "程序启动" << std::endl; Trace t; // 构造函数在此处调用 } ``` **输出**: ``` 程序启动 构造函数执行于运行时 ``` **结论**:对象的初始化明显晚于程序启动,发生在函数执行时。 --- #### **四、与静态对象的对比** | 对象型 | 内存分配时间 | 初始化时间 | 存储位置 | |---------------|--------------------|--------------------------|--------------| | **栈对象** | 函数调用时 | 声明处(运行时) | 栈区 | | **静态对象** | 程序加载时 | main()之前或首次使用时 | .data/.bss段 | | **全局对象** | 程序加载时 | main()执行前 | .data/.bss段 | --- #### **五、特殊场景分析** ##### **1. 编译器优化影响** - **优化可能**:若变量未被使用,编译器可能完全消除其分配(如`int unused;`) - **优化局限**:无法提前初始化,因程序行为可能依赖运行时状态 ```cpp int getSeed() { return time(NULL) % 100; } void demo() { int x = getSeed(); // 初始化值依赖运行时计算 } ``` ##### **2. 递归调用中的栈分配** ```cpp void recursive(int depth) { int local = depth; // 每次递归调用独立分配栈空间 if(depth > 0) recursive(depth-1); } ``` **行为分析**:每次递归调用生成新的栈帧,同一变量`local`在不同栈帧中有不同地址。 --- #### **六、底层机制图解** ``` 编译阶段 链接阶段 运行时 +-----------------+ +-----------------+ +-----------------+ | 源代码分析 | | 确定符号偏移量 | | 执行程序 | | 计算变量大小 | | 生成重定位信息 | | 调用函数 | | 生成中间代码 | +----> | 生成可执行文件 | +----> | 分配栈帧 | | (不分配内存) | | (仍无实际内存) | | 调整ESP寄存器 | +-----------------+ +-----------------+ +-----------------+ ``` --- #### **七、总结** - **编译时**:仅进行静态分析,生成管理栈的内存操作指令模板 - **运行时**: - **分配**:函数调用时通过修改栈指针一次性分配整个栈帧 - **初始化**:执行到变量声明处时进行赋值或调用构造函数 **核心区别**:编译阶段如同建筑师绘制蓝图,运行时才是工人实际建造房屋。栈对象的生存周期严格绑定于其作用域的执行流,这正是自动存储期(automatic storage duration)的核心特征。
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值