Item 04:确定对象被使用前已先被初始化

在C++编程中,确保对象在使用前被正确初始化至关重要,以避免不明确的行为。初始化不仅适用于内置类型,也适用于类的成员,需要在构造函数中通过成员初始化列表来完成。成员初始化次序遵循基类先于派生类,成员按声明顺序初始化的规则。同时,要注意不同编译单元内非局部静态对象的初始化次序问题,可以通过将非局部静态对象转换为局部静态对象来解决初始化次序不确定性。

Item 04:确定对象被使用前已先被初始化

Item 04: Make sure that objects are initialized before they’re used


读取未初始化的值会导致不明确的行为。而最佳的处理办法就是:永远在使用对象之前先将它初始化

对于内置类型,你必须手工完成此事。

对于内置类型以外的任何其他东西,初始化责任落在构造函数身上。规则很简答:确保每一个构造函数都将对象的每一个成员初始化

赋值和初始化

“确保每一个构造函数都将对象的每一个成员初始化”看起来很容易奉行,重要的是别混淆了赋值和初始化。

class PhoneNumber { ... };
class ABEntry {                 // ABEntry = “Address Book Entry”
public:
    ABEntry(const std::string& name, const std::string& address,
            const std::list<PhoneNumber>& phones);
private:
    std::string theName;
    std::string theAddress;
    std::list<PhoneNumber> thePhones;
    int numTimesConsulted;
};
ABEntry::ABEntry(const std::string& name, const std::string& address,
                 const std::list<PhoneNumber>& phones)
{
    theName = name;       // 这些都是赋值,
    theAddress = address; // 而非初始化
    thePhones = phones;
    numTimesConsulted = 0;
}

这会导致ABEntry对象带有你期望的值,但不是最佳做法。C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前。在ABEntry构造函数内,theName,theAddress和thePhones都不是被初始化,而是被赋值。初始化发生时间更早,发生于这些成员的默认构造函数被自动调用之时(比进入ABEntry构造函数本体的时间更早)。

ABEntry构造函数的比较好的写法是,使用所谓的成员初始化列表替换赋值动作:

ABEntry::ABEntry(const std::string& name, const std::string& address,
                 const std::list<PhoneNumber>& phones)
    : theName(name),
      theAddress(address),  // 这些都是初始化
      thePhones(phones),
      numTimesConsulted(0)
{}                          //构造函数本体不必有任何动作

这个构造函数和上一个的最终结果相同,但通常效率更高。基于赋值的那个版本首先调用默认构造函数为theName,theAddress和thePhones设初值,然后立刻再对它们赋予新值。默认构造函数的一切作为因此被浪费了。成员初始化列表的做法避免了这一问题,因为初值列表中针对各个成员变量而设的实参,被拿去作为各成员变量之构造函数的实参。本例中的theName以name为初值进行copy构造,theAddress以address为初值进行copy构造,thePhones以ponoes为初值进行copy构造。

对大多数类型而言,比起先调用默认构造函数然后再调用赋值操作符,仅仅调用一次copy构造函数是比较高效的,有时甚至高效的多。对于内置类型对象如numTimesConsulted,其初始化和赋值的成本相同,但为了一致性最好也通过成员初始化列表来初始化。

同样道理,甚至当你想要默认构造一个成员变量,你都可以使用成员列表初始化,只要指定无物作为初始化实参即可。加入ABEntry有一个无参数构造函数:

ABEntry::ABEntry()
    : theName(),            //调用theName的默认构造函数;
      theAddress(),         //调用theAddress的默认构造函数;
      thePhones(),          //调用thePhones的默认构造函数;
      numTimesConsulted(0)  //显示初始化为0
{} 

由于编译器会为用户自定义类型之成员变量自动调用默认构造函数——如果那些成员变量在“成员初始化列表”中没有被指定初值的话,因而引发某些程序员夸张地采用以上写法。这是可以理解的,但是一定要在成员初始化列表中列出所有成员变量,以免还得记住哪些成员变量可以无需初值。例如,由于numTimesConsulted属于内置类型,如果成员初始化列表遗漏了它,它就没有初值,因此可能会开启“不明确行为”的潘多拉盒子。

有些情况下,即使面对的成员变量属于内置类型(那么其初始化与赋值的成本相同),也一定得使用初始化类表。是的,如果成员变量是const或reference,它们就一定需要初值,不能被赋值。为避免需要记住成员变量何时必须在成员初始化列表中初始化,何时不需要,最简单粗暴的做法是:总是使用成员初始化列表。这样做有时候绝对必要,而且又往往比赋值更高效。

成员初始化次序

C++有着固定的“成员初始化次序”。次序总是相同:base classes更早与其dderived classes被初始化,而class的成员变量总是以其声明次序被初始化。看看ABEntry,其theName成员永远最先被初始化,然后是theAddress,再来是thePhone,最后是numTimesConsulted。即使它们在成员初始化列表中以不同的次序出现(编译器会报出警告),也不会有任何影响(只是报出警告)。为了避免代码阅读者的疑惑,或者必须一些晦涩错误(两个成员变量的初始化带有次序性,例如初始化数组时需要指定大小,因此代表大小的那个成员变量必须先有初值),当你在成员初始化列表中列出各个成员时,最好总是以其声明次序为次序

每个成员在构造函数初始化列表中只能指定一次。构造函数初始化列表仅指定用于初始化成员的值,并不指定这些初始化执行的次序。成员被初始化的次序就是定义成员的次序
————《C++ Primer》第四版 P389

不同编译单元内定义的non-local static对象的初始化次序

一旦你已经很小心地将“内置型成员变量”明确地加以初始化,而且也确保你的构造函数运用成员初始化列表初始化base classes和成员变量,那就只剩下一件事需要担心了,就是————“不同编译单元内定义的non-local static对象”的初始化次序。

所谓static对象

所谓static对象,其寿命从被构造出来直到程序结束为止,因此stack和heap-based对象都被排除。这种对象包括global对象、定义与namespace作用域内的对象、在classes内、在函数内、以及在file作用域内被声明为static的对象。函数内的static对象称为local static对象,其他static对象称为non-local static对象。程序结束时static对象会被自动销毁,也就是它们的析构函数会在main()结束时被自动调用。

所谓编译单元

所谓便一单元是指产出单一目标文件的那些源码。基本上它是单一源码文件加上其所含入的头文件。

现在,我们关心的问题涉及至少两个源码文件,每一个内含至少一个non-local static对象(也就是说该对象是global或位于namespace作用域内,抑或在class内或file作用域内被声明为static)。真正的问题是:如果某编译单元内的某个non-local static对象的初始化动作使用了另一个编译单元内的某个non-local static对象,它所用到的这个对象可能尚未被初始化,因为C++对“定义不同编译单元内的non-local static对象”的初始化次序并无明确定义。

假设你有一个FileSystem class,它让互联网上的文件看起来好像位于本机。由于这个class使世界看起来像个单一文件系统,你可能会产出一个特殊对象,位于global或namespace作用域内,象征单一文件系统:

class FileSystem {            // from your library’s header file
public:
    ...
    std::size_t numDisks() const; // one of many member functions
    ...
};
extern FileSystem tfs;  // declare object for clients to use
                        // (“tfs” = “the file system” );
                        // definition is in some .cpp file in your library

现在假设某些客户建立了一个class用以处理文件系统内的目录。很自然它们的class会用上tfs对象。

class Directory {       // created by library client
public:
    Directory( params );
    ...
};
Directory::Directory( params )
{
    ...
    std::size_t disks = tfs.numDisks(); // use the tfs object
    ...
}

进一步假设,这些客户决定创建一个Directory对象,用来放置临时文件:

Directory tempDir( params );    //directory for temporary files

现在初始化次序的重要性显现出来了:除非tfs在tempDir之前先被初始化,否则tempDir的构造函数会用到尚未初始化的tfs。但tfs和tempDir是不同的人在不同的时间于不同的源码文件建立起来的,它们是定义于不同编译单元内的non-local static对象。如何能够确定tfs会在tempDir之前先被初始化?

local static 替换 non-local static

幸运的是一个小小的设计可以完全消除这个问题。唯一需要做的是:将每个non-local static对象搬到自己的专属函数内(该对象在此函数内被声明为static)。这些函数返回一个reference指向它所含的对象。然后用户调用这些函数,而不直接指涉这些对象。换句话说,non-local static对象被local static对象替换了。这就是Singleton模式(单例模式)的一个常见手法。

这个手法的基础在于:C++保证,函数内的local static对象会在“该函数被调用期间”“首次遇上该对象之定义式”时被初始化。所以如果你以“函数调用”(返回一个reference指向local static对象)替换“直接访问non-local static对象”,你就获得了保证,保证你所获得的那个reference将指向一个历经初始化的对象。更棒的是,如果你从未调用non-local static对象的“仿真函数”,就绝不会引发构造和析构成本;真正的non-local static对象可没这等便宜!

class FileSystem { ... };   // as before
FileSystem& tfs()           // this replaces the tfs object; it could be
{                           // static in the FileSystem class
    static FileSystem fs;   // define and initialize a local static object
    return fs;              // return a reference to it
}
class Directory { ... };        // as before
Directory::Directory( params )  // as before, except references to tfs are
{                               // now to tfs()
    ...
    std::size_t disks = tfs().numDisks();
    ...
}
Directory& tempDir()                // this replaces the tempDir object; it
{                                   // could be static in the Directory class
    static Directory td( params );  // define/initialize local static object
    return td;                      // return reference to it
}

这种结构下的reference-returning函数往往十分单纯:第一行定义并初始化一个local static对象,第二行返回它。这样的单纯性使它们称为绝佳的inline候选人,尤其如果它们被频繁调用的话。但是从另一个角度看,这些函数“内含static对象”的事实使它们在多线程系统中带有不确定性。任何一种non-const static对象,不论它是local或non-local,在多线程环境下“等待某事发生”都会有麻烦。处理这个麻烦的一种做法是:在程序的单线程启动阶段手工调用所有reference-returning函数,这可消除与初始化有关的“竞速形势”。

运用reference-returning函数防止“初始化次序问题”,前提是其中有着一个对对象而言合理的初始化次序。如果你有一个系统,其中对象A必须在对象B之前先初始化,但A的初始化能否成功却又受制于B是否已初始化,这时候你就有麻烦了。坦白说你自作自受。只要避开如此病态的境况,此处描述的办法应该可以提供你良好的服务,至少在单线程程序中。

Note

  • 为内置型对象进行手工初始化,因为C++不保证初始化它们。
  • 构造函数最好使用成员初始化列表,而不要在构造函数本体内使用赋值操作。初始化列表中列出的成员变量,其排列次序应该和它们在class中的声明次序相同
  • 为免除“跨编译单元之初始化次序”问题,请以local static对象替换non-local static对象。

Effective C++

<think>我们遇到了一个运行时错误438:对象不支持该属性或方法。这通常发生在试图调用一个对象不支持的方法或属性时。 在InitializeCache过程中,这个错误可能发生在哪里? 可能的原因: 1. 在InitializeCache中,我们可能调用了某个对象不支持的方法或属性。 2. 可能是字典对象(Dictionary)未正确初始化,或者尝试调用了字典不支持的方法。 3. 也可能是工作表(Worksheet)对象不支持某个属性或方法。 回顾InitializeCache的代码,主要操作有: - 设置工作表对象:Set ws = ThisWorkbook.Sheets("投注数据") - 创建字典对象:Set animalNumbers = CreateObject("Scripting.Dictionary") - 遍历工作表的行,读取数据,并填充到字典中。 错误438可能出现在: a. 调用字典的方法时,比如animalNumbers.Add,但animalNumbers可能不是字典对象(如果CreateObject失败,则返回Nothing,但通常不会)。 b. 操作工作表时,比如ws.Cells(i, 1).Value,但ws可能不是工作表对象(如果工作表不存在,则ws为Nothing,此时调用任何属性或方法都会导致错误438?实际上,如果对象为Nothing,会报错91,而不是438)。 错误438更具体的指向是:我们调用了一个对象不支持的方法或属性。例如: - 如果animalNumbers是一个字典对象,但我们错误地使用了数组的方法,就会报错。 - 或者,我们可能错误地使用了工作表对象的某个属性。 由于错误发生在InitializeCache过程中,我们需要在代码中添加错误处理,以确定具体出错的行。 解决方案: 1. 添加详细的错误处理,定位出错的具体行。 2. 检查InitializeCache中的每一行,确保对象都被正确初始化,并且调用的方法和属性对象所支持的。 修改后的InitializeCache示例(添加错误处理):</think>### 解决 InitializeCache 错误 438: 对象不支持该属性或方法 错误 438 通常发生在尝试调用对象不支持的方法或属性时。以下是详细排查和解决方案: #### 错误原因分析 ```mermaid graph TD A[错误438] --> B[对象未正确初始化] A --> C[方法调用错误] A --> D[属性访问错误] A --> E[对象类型不匹配] B --> B1[字典未创建] B --> B2[工作表引用错误] C --> C1[错误的方法名] D --> D1[不存在的属性] E --> E1[对象类型错误] ``` #### 完整解决方案 ##### 1. 添加详细错误处理代码 ```vba Public Sub InitializeCache() On Error GoTo ErrorHandler Dim ws As Worksheet Set ws = ThisWorkbook.Sheets("投注数据") ' 验证工作表是否存在 If ws Is Nothing Then Err.Raise vbObjectError + 1001, , "工作表 '投注数据' 不存在" End If ' 创建字典对象 Dim animalNumbers As Object Set animalNumbers = CreateObject("Scripting.Dictionary") ' 检查字典是否成功创建 If animalNumbers Is Nothing Then Err.Raise vbObjectError + 1002, , "无法创建字典对象" End If ' 填充字典的代码... Exit Sub ErrorHandler: Dim errMsg As String errMsg = "错误 #" & Err.Number & ": " & Err.Description & vbCrLf & _ "发生在: " & ErlIfAvailable() & vbCrLf & _ "请检查相关对象是否已正确初始化" MsgBox errMsg, vbCritical, "初始化错误" End Sub ' 辅助函数:获取错误行号(如果可用) Private Function ErlIfAvailable() As String On Error Resume Next ErlIfAvailable = "行号: " & Erl If Err.Number <> 0 Then ErlIfAvailable = "行号未知" End Function ``` ##### 2. 常见错误场景及修复 **场景1: 字典对象未正确创建** ```vba ' 错误代码 Dim animalNumbers As Dictionary Set animalNumbers = New Dictionary ' 可能引发438错误 ' 正确代码 Dim animalNumbers As Object Set animalNumbers = CreateObject("Scripting.Dictionary") ``` **场景2: 工作表引用错误** ```vba ' 错误代码 Set ws = Sheets("投注数据") ' 可能引用错误的工作簿 ' 正确代码 Set ws = ThisWorkbook.Sheets("投注数据") ``` **场景3: 方法调用错误** ```vba ' 错误代码 animalNumbers.Add Key:="猪", Item:=Array(7, 19, 31, 43) ' 如果字典未初始化会报错 ' 安全调用 If Not animalNumbers Is Nothing Then animalNumbers.Add "猪", Array(7, 19, 31, 43) End If ``` ##### 3. 完整初始化流程验证 ```vba Public Sub SafeInitializeCache() On Error GoTo Cleanup ' 1. 验证应用程序环境 If Application.Version < 15 Then MsgBox "需要Excel 2013或更高版本", vbExclamation Exit Sub End If ' 2. 创建字典对象 Dim cache As Object Set cache = CreateObject("Scripting.Dictionary") cache.CompareMode = vbTextCompare ' 不区分大小写 ' 3. 获取数据工作表 Dim dataSheet As Worksheet Set dataSheet = GetOrCreateSheet("投注数据") ' 4. 填充缓存数据 PopulateAnimalData cache, dataSheet PopulateColorData cache, dataSheet ' 5. 存储到全局缓存 Set GlobalCache = cache MsgBox "缓存初始化成功! 缓存项数量: " & cache.Count, vbInformation Exit Sub Cleanup: If Err.Number <> 0 Then MsgBox "初始化失败: " & Err.Description & vbCrLf & _ "错误代码: " & Err.Number, vbCritical End If Set cache = Nothing End Sub Private Function GetOrCreateSheet(sheetName As String) As Worksheet On Error Resume Next Set GetOrCreateSheet = ThisWorkbook.Sheets(sheetName) If GetOrCreateSheet Is Nothing Then Set GetOrCreateSheet = ThisWorkbook.Sheets.Add GetOrCreateSheet.Name = sheetName ' 初始化工作表结构 InitializeSheetStructure GetOrCreateSheet End If End Function Private Sub InitializeSheetStructure(ws As Worksheet) With ws .Range("A1").Value = "号码" .Range("B1").Value = "金额" ' 填充1-49号码 Dim i As Long For i = 1 To 49 .Cells(i + 1, 1).Value = Format(i, "00") .Cells(i + 1, 2).Value = 0 Next i End With End Sub ``` #### 调试技巧 1. **立即窗口调试**: ```vba ' 在立即窗口中测试 Set ws = ThisWorkbook.Sheets("投注数据") ? ws.Name ' 查看返回结果 Set dict = CreateObject("Scripting.Dictionary") ? TypeName(dict) ' 应返回"Dictionary" ``` 2. **错误断点设置**: - 在VBA编辑器中:`调试 > 添加监视` - 输入表达式:`Err.Number <> 0` - 选择选项:`当监视值为真时中断` 3. **对象浏览器检查**: - 按F2打开对象浏览器 - 搜索"Dictionary"查看可用方法和属性 - 确保使用的方法存在于对象中 #### 预防措施 1. **添加类型检查**: ```vba If TypeName(animalNumbers) <> "Dictionary" Then MsgBox "字典对象类型错误: " & TypeName(animalNumbers), vbCritical Exit Sub End If ``` 2. **使用早期绑定(需引用)**: ```vba ' 工具 > 引用 > 勾选 "Microsoft Scripting Runtime" Dim animalNumbers As Scripting.Dictionary Set animalNumbers = New Scripting.Dictionary ``` 3. **实现缓存验证**: ```vba Public Function IsCacheInitialized() As Boolean On Error Resume Next IsCacheInitialized = (GlobalCache.Count > 0) If Err.Number <> 0 Then IsCacheInitialized = False End Function ``` #### 完整工作流程 1. 在`ThisWorkbook`模块中添加: ```vba Private Sub Workbook_Open() SafeInitializeCache End Sub ``` 2. 在标准模块中添加全局变量: ```vba Public GlobalCache As Object ``` 3. 运行`SafeInitializeCache`进行初始化 通过以上方案,您应该能够解决错误438并成功初始化缓存。如果问题仍然存在,错误处理程序将提供更详细的错误信息以便进一步诊断。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值