C++学习笔记十二-类
一、概述:类类型常被称为抽象数据类型(abstract data types)。抽象数据类型将数据(即状态)和作用于状态的操作视为一个单元。我们可以抽象地考虑类该做什么,而无须知道类如何去完成这些操作。通过类我们能够将实现和接口分离,用接口指定类所支持的操作,而实现的细节只需类的实现者了解或关心。抽象数据类型是面向对象编程和泛型编程的基础。它们用起来与内置类型一样容易和直观。
1.最简单地说,类就是定义了一个新的类型和一个新作用域。
2.每个类可以没有成员,也可以定义多个成员,成员可以是数据、函数或类型别名。一个类可以包含若干公有的、私有的和受保护的部分。在 public 部分定义的成员可被使用该类型的所有代码访问;在 private 部分定义的成员可被其他类成员访问。所有成员必须在类的内部声明。
3.创建一个类类型的对象时,编译器会自动使用一个构造函数来初始化该对象。构造函数是一个特殊的、与类同名的成员函数,用于给每个数据成员设置适当的初始值。构造函数一般就使用一个构造函数初始化列表,来初始化对象的数据成员:
// default constructor needed to initialize members of built-in type Sales_item(): units_sold(0), revenue(0.0) { }
4.const 成员函数不能改变其所操作的对象的数据成员。const 必须同时出现在声明和定义中,若只出现在其中一处,就会出现一个编译时错误。
5.类的定义格式
类的定义格式一般地分为说明部分和实现部分。说明部分是用来说明该类中的成员,包含数据成员的说明和成员函数的说明。成员函数是用来对数据成员进行操作的,又称为“方法”。实现部分是用来对成员函数的定义。概括说来,说明部分将告诉使用者“干什么”,而实现部分是告诉使用者“怎么干”。
类的一般定义格式如下:
class <类名>
{
public:
<成员函数或数据成员的说明>
private:
<数据成员或成员函数的说明>
};
<各个成员函数的实现>
6.定义类时应注意的事项
1、在类体中不允许对所定义的数据成员进行初始化。
2、类中的数据成员的类型可以是任意的,包含整型、浮点型、字符型、数组、指针和引用等。也可以是对象。另一个类的对象,可以作该类的成员,但是自身类的对象是不可以的,而自身类的指针或引用又是可以的。当一个类的对象用为这个类的成员时,如果另一个类的的定义在后,需要提前说明。
3、一般地,在类体内先说明公有成员,它们是用户所关心的,后说明私有成员,它们是用户不感兴趣的。在说明数据成员时,一般按数据成员的类型大小,由小至大说明,这样可提高时空利用率。
4、经常习惯地将类定义的说明部分或者整个定义部分(包含实现部分)放到一个头文件中。
在类的声明和成员函数的实现(即函数的定义)分开写时,在类的声明部分中,不用写出变量名,只要声明参数的类型即可. 通常将成员函数写在前面,数据成员写在后面.
二、数据抽象和封装:
1.数据抽象是一种依赖于接口和实现分离的编程(和设计)技术。类设计者必须关心类是如何实现的,但使用该类的程序员不必了解这些细节。相反,使用一个类型的程序员仅需了解类型的接口,他们可以抽象地考虑该类型做什么,而不必具体地考虑该类型如何工作。
2.封装是一项低层次的元素组合起来的形成新的、高层次实体珠技术。被封装的元素隐藏了它们的实现细节——可以调用一个函数但不能访问它所执行的语句。同样地,类也是一个封装的实体:它代表若干成员的聚焦,大多数(良好设计的)类类型隐藏了实现该类型的成员。
3.可以在任意的访问标号出现之前定义类成员。在类的左花括号之后、第一个访问标号之前定义成员的访问级别,其值依赖于类是如何定义的。如果类是用 struct 关键字定义的,则在第一个访问标号之前的成员是公有的;如果类是用 class 关键字是定义的,则这些成员是私有的。
4.具体类型和抽象类型:并非所有类型都必须是抽象的。标准库中的 pair 类就是一个实用的、设计良好的具体类而不是抽象类。具体类会暴露而非隐藏其实现细节。一些类,例如 pair,确实没有抽象接口。pair 类型只是将两个数据成员捆绑成单个对象。在这种情况下,隐藏数据成员没有必要也没有明显的好处。在像 pair 这样的类中隐藏数据成员只会造成类型使用的复杂化。
5.在简单的应用程序中,类的使用者和设计者也许是同一个人。即使在这种情况下,保持角色区分也是有益的。设计类的接口时,设计者应该考虑的是如何方便类的使用;使用类的时候,设计者就不应该考虑类如何工作。C++ 程序员经常会将应用程序的用户和类的使用者都称为“用户”。
6.成员函数可以重载
7.inline 成员函数:在类内部定义的成员函数,将自动作为 inline 处理。可以在类定义体内部指定一个成员为inline,作为其声明的一部分。或者,也可以在类定义外部的函数定义上指定 inline。在声明和定义处指定 inline 都是合法的。在类的外部定义 inline 的一个好处是可以使得类比较容易阅读。(详见inline函数)
8.类声明与类定义:一旦遇到右花括号,类的定义就结束了。并且一旦定义了类,那以我们就知道了所有的类成员,以及存储该类的对象所需的存储空间。
A).在一个给定的源文件中,一个类只能被定义一次。如果在多个文件中定义一个类,那么每个文件中的定义必须是完全相同的。
B).可以声明一个类而不定义它:
class Screen; // declaration of the Screen class
这个声明,有时称为前向声明(forward declaraton),在程序中引入了类类型的 Screen。在声明之后、定义之前,类 Screen 是一个不完全类型(incompete type),即已知 Screen 是一个类型,但不知道包含哪些成员。
不完全类型(incomplete type)只能以有限方式使用。不能定义该类型的对象。不完全类型只能用于定义指向该类型的指针及引用,或者用于声明(而不是定义)使用该类型作为形参类型或返回类型的函数。
C).在创建类的对象之前,必须完整地定义该类。必须定义类,而不只是声明类,这样,编译器就会给类的对象预定相应的存储空间。同样地,在使用引用或指针访问类的成员之前,必须已经定义类。
D).为类的成员使用类声明:只有当类定义已经在前面出现过,数据成员才能被指定为该类类型。如果该类型是不完全类型,那么数据成员只能是指向该类类型的指针或引用。
因为只有当类定义体完成后才能定义类,因此类不能具有自身类型的数据成员。然而,只要类名一出现就可以认为该类已声明。因此,类的数据成员可以是指向自身类型的指针或引用:
class LinkScreen { Screen window; LinkScreen *next; LinkScreen *prev; };
9.类对象:定义一个类时,也就是定义了一个类型。一旦定义了类,就可以定义该类型的对象。定义对象时,将为其分配存储空间,但(一般而言)定义类型时不进行存储分配.
1.定义了一个类类型之后,可以按以下两种方式使用。
A.将类的名字直接用作类型名。
B.指定关键字 class 或 struct,后面跟着类的名字:
Sales_item item1; // default initialized object of type Sales_item class Sales_item item1; // equivalent definition of item1
两种引用类类型方法是等价的。第二种方法是从 C 继承而来的,在 C++ 中仍然有效。第一种更为简练,由 C++ 语言引入,使得类类型更容易使用。
2.为什么类的定义以分号结束?
类的定义分号结束。分号是必需的,因为在类定义之后可以接一个对象定义列表。定义必须以分号结束:
class Sales_item { /* ... */ }; class Sales_item { /* ... */ } accum, trans;
通常,将对象定义成类定义的一部分是个坏主意。这样做,会使所发生的操作难以理解。对读者而言,将两个不同的实体(类和变量)组合在一个语句中,也会令人迷惑不解。
三、隐含的 this 指针:成员函数具有一个附加的隐含形参,即指向该类对象的一个指针。这个隐含形参命名为 this,与调用成员函数的对象绑定在一起。成员函数不能定义 this 形参,而是由编译器隐含地定义。成员函数的函数体可以显式使用 this 指针,但不是必须这么做。如果对类成员的引用没有限定,编译器会将这种引用处理成通过 this 指针的引用。
1.何时使用 this 指针:尽管在成员函数内部显式引用 this 通常是不必要的,但有一种情况下必须这样做:当我们需要将一个对象作为整体引用而不是引用对象的一个成员时。最常见的情况是在这样的函数中使用 this:该函数返回对调用该函数的对象的引用。如下列情形:
希望用户能够将这些操作的序列连接成一个单独的表达式:
// move cursor to given position, and set that character myScreen.move(4,0).set('#');
在单个表达式中调用 move 和 set 操作时,每个操作必须返回一个引用,该引用指向执行操作的那个对象:
class Screen { public: // interface member functions Screen& move(index r, index c); Screen& set(char); Screen& set(index, index, char); // other members as before };
//这些函数的返回类型是 Screen&,指明该成员函数返回对其自身类类型的对象的引用。每个函数都返回调用自己的那个对象。使用 this 指针来访问该对象。
Screen& Screen::set(char c) { contents[cursor] = c; return *this; } Screen& Screen::move(index r, index c) { index row = r * width; // row location cursor = row + c; return *this; }
3.从 const 成员函数返回 *this.
不能从 const 成员函数返回指向类对象的普通引用。const 成员函数只能返回 *this 作为一个 const 引用。
4.基于 const 的重载:const 成员函数重载可看做是对隐含的指针this的参数重载。(详见:Const 重载解析)
5.可变数据成员:有时(但不是很经常),我们希望类的数据成员(甚至在 const 成员函数内)可以修改。这可以通过将它们声明为 mutable 来实现。
可变数据成员(mutable data member)永远都不能为 const,甚至当它是 const 对象的成员时也如此。因此,const 成员函数可以改变 mutable 成员。要将数据成员声明为可变的,必须将关键字 mutable 放在成员声明之前:
class Screen { public: // interface member functions private: mutable size_t access_ctr; // may change in a const members // other data members as before }; //我们给 Screen 添加了一个新的可变数据成员 access_ctr。使用 access_ctr 来跟踪调用 Screen 成员函数的频繁程度: void Screen::do_display(std::ostream& os) const { ++access_ctr; // keep count of calls to any member function os << contents; }
尽管 do_display 是 const,它也可以增加 access_ctr。该成员是可变成员,所以,任意成员函数,包括 const 函数,都可以改变 access_ctr 的值。
四、类的作用域
通常来说,一段程序代码中所用到的名字并不总是有效/可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域。
作用域的使用提高了程序逻辑的局部性,增强程序的可靠性,减少名字冲突。
在 C++ 程序中,所有名字必须在使用之前声明。
1.每个类都定义了自己的新作用域和唯一的类型。在类的定义体内声明类成员,将成员名引入类的作用域。两个不同的类具有两个的类作用域。
2.在类作用域之外,成员只能通过对象或指针分别使用成员访问操作符 . 或 -> 来访问。这些操作符左边的操作数分别是一个类对象或指向类对象的指针。跟在操作符后面的成员名字必须在相关联的类的作用域中声明
一些成员使用成员访问操作符来访问,另一些直接通过类使用作用域操作符(::)来访问。一般的数据或函数成员必须通过对象来访问。定义类型的成员,如 Screen::index,使用作用域操作符来访问。
3.尽管成员是在类的定义体之外定义的,但成员定义就好像它们是在类的作用域中一样。
4.形参表和函数体处于类作用域中:在定义于类外部的成员函数中,形参表和成员函数体都出现在成员名之后。这些都是在类作用域中定义,所以可以不用限定而引用其他成员。
5.函数返回类型不一定在类作用域中:与形参类型相比,返回类型出现在成员名字前面。如果函数在类定义体之外定义,则用于返回类型的名字在类作用域之外。如果返回类型使用由类定义的类型,则必须使用完全限定名。
6.必须在类中先定义类型名字,才能将它们用作数据成员的类型,或者成员函数的返回类型或形参类型。
7.编译器按照成员声明在类中出现的次序来处理它们。通常,名字必须在使用之前进行定义。而且,一旦一个名字被用作类型名,该名字就不能被重复定义:
typedef double Money; class Account { public: Money balance() { return bal; } // uses global definition of Money private: // error: cannot change meaning of Money typedef long double Money; Money bal; // ... };
8.类作用域中的名字查找:
A.首先,在使用该名字的块中查找名字的声明。只考虑在该项使用之前声明的名字。
B.如果找不到该名字,则在包围的作用域中查找。
如果找不到,则编译错误
类定义实际上是在两个阶段中处理:首先,编译成员声明;只有在所有成员出现之后,才编译它们的定义本身。
9.类成员声明的名字查找:
A.按以下方式确定在类成员的声明中用到的名字。
B.检查出现在名字使用之前的类成员的声明。
如果第 A步查找不成功,则检查包含类定义的作用域中出现的声明以及出现在类定义之前的声明。
必须在类中先定义类型名字,才能将它们用作数据成员的类型,或者成员函数的返回类型或形参类型。
编译器按照成员声明在类中出现的次序来处理它们。通常,名字必须在使用之前进行定义. 而且,一旦一个名字被用作类型名,该名字就不能被重复定义,详见:C++作用域:
typedef double Money; class Account { public: Money balance() { return bal; } // uses global definition of Money private: // error: cannot change meaning of Money typedef long double Money; Money bal; // ... };
10.类成员定义中的名字查找:
按以下方式确定在成员函数的函数体中用到的名字。
A.首先检查成员函数局部作用域中的声明。
B.如果在成员函数中找不到该名字的声明,则检查对所有类成员的声明。
C.如果在类中找不到该名字的声明,则检查在此成员函数定义之前的作用域中出现的声明。
11.尽管全局对象被屏蔽了,但通过用全局作用域确定操作符来限定名字,仍然可以使用它。
// bad practice: Don't hide names that are needed from surrounding scopes void dummy_fcn(index height) { cursor = width * ::height;// which height? The global one }
五、构造函数
概述:构造函数是特殊的成员函数,只要创建类类型的新对象,都要执行构造函数。构造函数的工作是保证每个对象的数据成员具有合适的初始值。只要创建该类型的一个对象,编译器就运行一个构造函数
1.构造函数初始化列表
与任何其他函数一样,构造函数具有名字、形参表和函数体。与其他函数不同的是,构造函数也可以包含一个构造函数初始化列表:
// recommended way to write constructors using a constructor initializer Sales_item::Sales_item(const string &book): isbn(book), units_sold(0), revenue(0.0) { }
2.在构造函数初始化列表中没有显式提及的每个成员,使用与初始化变量相同的规则来进行初始化。运行该类型的默认构造函数,来初始化类类型的数据成员。内置或复合类型的成员的初始值依赖于对象的作用域:在局部作用域中这些成员不被初始化,而在全局作用域中它们被初始化为 0。
3.概念上讲,可以认为构造函数分两个阶段执行:(1)初始化阶段;(2)普通的计算阶段。计算阶段由构造函数函数体中的所有语句组成。不管成员是否在构造函数初始化列表中显式初始化,类类型的数据成员总是在初始化阶段初始化。初始化发生在计算阶段开始之前。
4.每个成员在成员初始化表中只能出现一次,初始化的顺序不是由名字在初始化表中的顺序决定,而是由成员在类中被声明的顺序决定的。按照与成员声明一致的次序编写构造函数初始化列表是个好主意。此外,尽可能避免使用成员来初始化其他成员。
5.注意:构造函数的初始化列表只在构造函数的定义中指定,而不在声明中指定.
6.必须对任何 const 或引用类型成员以及没有默认构造函数的类类型的任何成员使用初始化式。当类成员需要使用初始化列表时,通过常规地使用构造函数初始化列表,就可以避免发生编译时错误。
7.初始化式可以是任意表达式:一个初始化式可以是任意复杂的表达式。例如,可以给 Sales_item 类一个新的构造函数,该构造函数接受一个 string 表示 isbn,一个 usigned 表示售出书的数目,一个 double 表示每本书的售出价格:
Sales_item(const std::string &book, int cnt, double price): isbn(book), units_sold(cnt), revenue(cnt * price) { }
8.类类型的数据成员的初始化式:初始化类类型的成员时,要指定实参并传递给成员类型的一个构造函数。可以使用该类型的任意构造函数。例如,Sales_item 类可以使用任意一个 string 构造函数来初始化 isbn,也可以用 ISBN 取值的极限值来表示 isbn 的默认值,而不是用空字符串。可以将 isbn 初始化为由 10 个 9 构成的串:
// alternative definition for Sales_item default constructor Sales_item(): isbn(10, '9'), units_sold(0), revenue(0.0) {}
9.默认构造函数:只要定义一个对象时没有提供初始化式,就使用默认构造函数。为所有形参提供默认实参的构造函数也定义了默认构造函数。
A.一个类哪怕只定义了一个构造函数,编译器也不会再生成默认构造函数。这条规则的根据是,如果一个类在某种情况下需要控制对象初始化,则该类很可能在所有情况下都需要控制。
B.只有当一个类没有定义构造函数时,编译器才会自动生成一个默认构造函数。
C.合成的默认构造函数(synthesized default constructor)使用与变量初始化相同的规则来初始化成员。具有类类型的成员通过运行各自的默认构造函数来进行初始化。内置和复合类型的成员,如指针和数组,只对定义在全局作用域中的对象才初始化。当对象定义在局部作用域中时,内置或复合类型的成员不进行初始化。
D. 如果类包含内置或复合类型的成员,则该类不应该依赖于合成的默认构造函数。它应该定义自己的构造函数来初始化这些成员。
E.此外,每个构造函数应该为每个内置或复合类型的成员提供初始化式。没有初始化内置或复合类型成员的构造函数,将使那些成员处于未定义的状态。除了作为赋值的目标之外,以任何方式使用一个未定义的成员都是错误的。如果每个构造函数将每个成员设置为明确的已知状态,则成员函数可以区分空对象和具有实际值的对象。
F.使用默认构造函数:
// oops! declares a function, not an object Sales_item myobj();
下面这段代码也是正确的:
// ok: create an unnamed, empty Sales_itemand use to initialize myobj Sales_item myobj = Sales_item();
10.隐式类类型转换:,C++ 语言定义了内置类型之间的几个自动转换。也可以定义如何将其他类型的对象隐式转换为我们的类类型,或将我们的类类型的对象隐式转换为其他类型。
可以用单个实参来调用的构造函数定义了从形参类型到该类类型的一个隐式转换。
让我们再看看定义了两个构造函数的 Sales_item 版本:
class Sales_item { public: // default argument for book is the empty string Sales_item(const std::string &book = ""): isbn(book), units_sold(0), revenue(0.0) { } Sales_item(std::istream &is); // as before };
这里的每个构造函数都定义了一个隐式转换。因此,在期待一个 Sales_item 类型对象的地方,可以使用一个 string 或一个 istream:
string null_book = "9-999-99999-9"; // ok: builds a Sales_itemwith 0 units_soldand revenue from // and isbn equal to null_book item.same_isbn(null_book);
11.抑制由构造函数定义的隐式转换:可以通过将构造函数声明为 explicit,来防止在需要隐式转换的上下文中使用构造函数.
explicit 关键字只能用于类内部的构造函数声明上。在类的定义体外部所做的定义上不再重复它.
12. 为转换而显式地使用构造函数:
除非有明显的理由想要定义隐式转换,否则,单形参构造函数应该为 explicit。将构造函数设置为 explicit 可以避免错误,并且当转换有用时,用户可以显式地构造对象。
任何构造函数都可以用来显式地创建临时对象。
13.类成员的显式初始化:
尽管大多数对象可以通过运行适当的构造函数进行初始化,但是直接初始化简单的非抽象类的数据成员仍是可能的。对于没有定义构造函数并且其全体数据成员均为 public 的类,可以采用与初始化数组元素相同的方式初始化其成员:
struct Data { int ival; char *ptr; }; // val1.ival = 0; val1.ptr = 0 Data val2 = { 1024, "Anna Livia Plurabelle" };
根据数据成员的声明次序来使用初始化式。
六、友元:友元机制允许一个类将对其非公有成员的访问权授予指定的函数或类。友元的声明以关键字 friend 开始。它只能出现在类定义的内部。友元声明可以出现在类中的任何地方:友元不是授予友元关系的那个类的成员,所以它们不受声明出现部分的访问控制影响。通常,将友元声明成组地放在类定义的开始或结尾是个好主意。
1.友元声明与作用域:更一般地讲,必须先定义包含成员函数的类,才能将成员函数设为友元。另一方面,不必预先声明类和非成员函数来将它们设为友元。
2.重载函数与友元关系:类必须将重载函数集中每一个希望设为友元的函数都声明为友元:
// overloaded storeOn functions extern std::ostream& storeOn(std::ostream &, Screen &); extern BitMap& storeOn(BitMap &, Screen &); class Screen { // ostream version of storeOn may access private parts of Screen objects friend std::ostream& storeOn(std::ostream &, Screen &); // ... };
类 Screen 将接受一个 ostream& 的 storeOn 版本设为自己的友元。接受一个 BitMap& 的版本对 Screen 没有特殊访问权。
3.在类中定义的友元函数不应被视为在全局命名空间范围中定义和声明了它。 但是,可以通过依赖于参数的查找找到它。
// C3767e.cpp namespace N { class C { friend void FriendFunc() {} friend void AnotherFriendFunc(C* c) {} }; } int main() { using namespace N; FriendFunc(); // C3767 error C* pC = new C(); AnotherFriendFunc(pC); // found via argument-dependent lookup }
4.当我们将成员函数声明为友元时,函数名必须用该函数所属的类名字加以限定。
5.定义在类内部的友元函数,也是内联函数,
七 、static成员
1.通常,非 static 数据成员存在于类类型的每个对象中。不像普通的数据成员,static 数据成员独立于该类的任意对象而存在;每个 static 数据成员是与类关联的对象,并不与该类的对象相关联。
2.正如类可以定义共享的 static 数据成员一样,类也可以定义 static 成员函数。static 成员函数没有 this 形参,它可以直接访问所属类的 static 成员,但不能直接使用非 static 成员
3.使用 static 成员而不是全局对象有三个优点。
A。static 成员的名字是在类的作用域中,因此可以避免与其他类的成员或全局对象名字冲突。
B。可以实施封装。static 成员可以是私有成员,而全局对象不可以。
C。通过阅读程序容易看出 static 成员是与特定类关联的。这种可见性可清晰地显示程序员的意图。
4.使用类的 static 成员:可以通过作用域操作符从类直接调用 static 成员,或者通过对象、引用或指向该类类型对象的指针间接调用。像使用其 他成员一样,类成员函数可以不用作用域操作符来引用类的 static 成员
5.static 成员函数:Account 类有两个名为 rate 的 static 成员函数,其中一个定义在类的内部。当我们在类的外部定义 static 成员时,无须重复指定 static 保留字,该保留字只出现在类定义体内部的声明处:
class Account { public: // interface functions here void applyint() { amount += amount * interestRate; } static double rate() { return interestRate; } static void rate(double); // sets a new rate private: std::string owner; double amount; static double interestRate; static double initRate(); }; void Account::rate(double newRate) { interestRate = newRate; }
6.static 函数没有 this 指针:
A。static 成员是类的组成部分但不是任何对象的组成部分,因此,static 成员函数没有 this 指针。通过使用非 static 成员显式或隐式地引用 this 是一个编译时错误。
B。因为 static 成员不是任何对象的组成部分,所以 static 成员函数不能被声明为 const。毕竟,将成员函数声明为 const 就是承诺不会修改该函数所属的对象。最后,static 成员函数也不能被声明为虚函数。
7.static 数据成员:
A。static 数据成员可以声明为任意类型,可以是常量、引用、数组、类类型,等等。
B。static 数据成员必须在类定义体的外部定义(正好一次)。不像普通数据成员,static 成员不是通过类构造函数进行初始化,而是应该在定义时进行初始化。
C。定义 static 数据成员的方式与定义其他类成员和变量的方式相同:先指定类型名,接着是成员的完全限定名。
可以定义如下 interestRate:
// define and initialize static class member double Account::interestRate = initRate(); D。像使用任意的类成员一样,在类定义体外部引用类的 static 成员时,必须指定成员是在哪个类中定义的。然而,static 关键字只能用于类定义体内部的声明中,定义不能标示为 static。
8.特殊的整型 const static 数据成员:
A。一般而言,类的 static数据成员,像普通数据成员一样,不能在类的定义体中初始化。相反,static 数据成员通常在定义时才初始化。
B。这个规则的一个例外是,只要初始化式是一个常量表达式,整型 const static 数据成员就可以在类的定义体中进行初始化:
class Account { public: static double rate() { return interestRate; } static void rate(double); // sets a new rate private: static const int period = 30; // interest posted every 30 days double daily_tbl[period]; // ok: period is constant expression };
C。用常量值初始化的整型 const static 数据成员是一个常量表达式。同样地,它可以用在任何需要常量表达式的地方。
D。const static 数据成员在类的定义体中初始化时,该数据成员仍必须在类的定义体之外进行定义。如不定义也可编译正常,但有本质区别
#include <iostream>
class A
{
public:
static const int a = 10;
};
//const int A::a;
int main()
{
std::cout << &A::a << std::endl;//无法编译通过,会显示“undefined reference to A::a 的错误”.去掉const int A::a的注释后,运行正常
}
//被注释掉那句要不要加,为什么?
//加与不加的主要区别是,类外加上定义后,才会为a申请空间,否则,a只能作为常量表达式使用,在编译结束后a就没了,不能对其做取地址操作。
9.static 成员不是类对象的组成部分:普通成员都是给定类的每个对象的组成部分。static 成员独立于任何对象而存在,不是类类型对象的组成部分。因为 static 数据成员不是任何对象的组成部分,所以它们的使用方式对于非 static 数据成员而言是不合法的。
A。例如,static 数据成员的类型可以是该成员所属的类类型。非 static 成员被限定声明为其自身类对象的指针或引用:
class Bar { public: // ... private: static Bar mem1; // ok Bar *mem2; // ok Bar mem3; // error };
B。类似地,static 数据成员可用作默认实参:
class Screen { public: // bkground refers to the static member // declared later in the class definition Screen& clear(char = bkground); private: static const char bkground = '#'; };
非 static 数据成员不能用作默认实参,因为它的值不能独立于所属的对象而使用。使用非 static 数据成员作默认实参,将无法提供对象以获取该成员的值,因而是错误的。