1、构造函数:
构造函数的工作是保证每个对象的数据成员具有合适的初始值。
class Father { public: Father(int _i):i(_i),j(3){} //在构造函数中,初始化那些未动态赋值的成员变量。 private: int i; int j; };
类的定义以分号结束是必需的,因为在类定义之后可以接一个对象定义列表。
class Father{/*...*/}; class Father{/*...*/} obj1,obj2;
但通常,将对象定义成类定义的一部分是个坏主意。
2、成员函数:
在类内部定义的函数默认为 inline,在类外部定义的成员函数必须指明它们是在类的作用域中。成员函数有一个附加的隐含实参,将函数绑定到调用函数的对象。将关键字 const加在形参表之后,就可以将成员函数声明为常量。const 成员不能改变其所操作的对象的数据成员。const 必须同时出现在声明和定义中,若只出现在其中一处,就会出现一个编译时错误。
3、定义类类型的对象:
将类的名字直接用作类型名,或指定关键字 class 或 struct,后面跟着类的名字:
Father obj1; class Father obj2;
两种引用类类型方法是等价的。第二种方法是从 C 继承而来的,在 C++ 中仍然有效。
4、何时使用 this 指针:
尽管在成员函数内部显式引用 this 通常是不必要的,但有一种情况下必须这样做:当我们需要将一个对象作为整体引用而不是引用对象的一个成员时。最常见的情况是在这样的函数中使用 this:该函数返回对调用该函数的对象的引用。
Father& get() { return *this; }
5、const 函数的重载:
class Father { public: Father():j(3){} void setj(int _j){j = _j;} void setj(int _j) const{cout<<"this is const fun!"<<endl;} public: int j; }; int main() { Father f1; const Father f2; f1.setj(10); f2.setj(10); cout<<"j: "<<f1.j<<endl; cout<<"j: "<<f2.j<<endl; }
6、可变数据成员:
class Father { public: void setj(int _j) const{j = _j;} public: mutable int j; }; int main() { Father f; f.setj(10); cout<<"j: "<<f.j<<endl; }
声明为 mutable 的成员变量,永远都不能为 const,甚至当它是 const 对象的成员时也如此。因此,const 成员函数可以改变 mutable 成员。
7、有时需要构造函数初始化列表:
class Father { public: Father(int _j,int _i):j(_j){i = _i;} //Father(int _j,int _i){j=_j;i=_i;} //Error 上面是初始化,这里相当于赋值 int getj(){return j;} public: int i; const int j; };
记住,可以初始化 const 对象或引用类型的对象,但不能对它们赋值。初始化 const 或引用类型数据成员的唯一机会是构造函数初始化列表中。构造函数初始化列表仅指定用于初始化成员的值,并不指定这些初始化执行的次序。成员被初始化的次序就是定义成员的次序,上面的程序,就是依次初始化 i 和 j
8、使用默认实参来构造函数,可以减少代码重复。
class Father { public: //Father(int _i = 10,int _j):i(_i),j(_j){} //默认实参必须位于参数表后面 Father(int _i,int _j = 10):i(_i),j(_j){} int i,j; }; int main() { Father f(3); cout<<"i: "<<f.i<<"\nj: "<<f.j<<endl; }
为所有形参提供默认实参的构造函数也相当于定义了默认构造函数。每个构造函数应该为每个内置或复合类型的成员提供初始化式。没有初始化内置或复合类型成员的构造函数,将使那些成员处于未定义的状态。除了作为赋值的目标之外,以任何方式使用一个未定义的成员都是错误的。
9、构造函数的隐式转换:
class Father { public: Father(int _i):i(_i),j(10){} //Father(int _i,int _j = 10):i(_i),j(_j){} //这种情况也可以隐式转换 int i,j; }; int main() { Father f = 20; //相当于 Father f(20); 当调用的构造函数只有一个参数时,且构造函数前没有 explicit 关键字修饰时,可以用赋值的方式隐式转换。 //cout<<"i:"<<Father(20).i<<endl; //即使有 explicit 关键字修饰,也可以使用此显式转换,创建临时对象 cout<<"i: "<<f.i<<"\nj: "<<f.j<<endl; }
抑制这种转换,可以在构造函数前使用 explicit 关键字。该关键字只能用于类内部的构造函数声明上。在类的定义体外部所做的定义上不需要再重复。
10、类成员的显式初始化:
当全体类成员都为 public 时,可以像结构体那样使用 User user = {1,"name","pwd"}; 来初始化。但不推荐,因为如果增加或删除一个成员,修改的地方可能非常多。
11、友元可以是普通的非成员函数,或前面定义的其他类的成员函数,或整个类。将一个类设为友元,友元类的所有成员函数都可以访问授予友元关系的那个类的非公有成员。
class Father { public: Father(int _i,int _j):i(_i),j(_j){} friend void print(); //只能在此处声明 friend private: int i,j; }; void print() //由于是Father类的友元函数,故可直接访问其对象的属性 { Father f(10,20); cout<<"i:"<<f.i<<" j:"<<f.j<<endl; } int main() { print(); }
12、static 成员独立于任何对象而存在,不是类类型对象的组成部分。
正如类可以定义共享的 static 数据成员一样,类也可以定义 static 成员函数。static 成员函数没有 this 形参,它可以直接访问所属类的 static 成员,但不能直接使用非 static 成员。类的任何对象都可以访问类的 static 成员,另外通过域操作符也可以从类直接调用 static成员。static 函数没有 this 指针。因为 static 成员不是任何对象的组成部分,所以 static 成员函数不能被声明为 const。毕竟,将成员函数声明为 const 就是承诺不会修改该函数所属的对象。最后,static 成员函数也不能被声明为虚函数。static 数据成员一般不在类定义体中初始化,但有一个例外,就是只要初始化式是一个常量表达式,整型 const static 数据成员就可以在类的定义体中进行初始化。
//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 = '#';
//int i = 3; //Error! 不允许在类内初始化非常量静态成员 ‘i’
};
13、需要复制构造函数的地方:
1). 一个对象以值传递的方式传入函数体
2). 一个对象以值传递的方式从函数返回
3). 一个对象需要通过另外一个对象进行初始化
14、直接初始化和复制初始化,对于内置类型来说,没有区别。但对于类类型来说,直接初始化直接是调用相应的构造函数,复制初始化首先使用指定构造函数创建一个临时对象,然后用复制构造函数将那个临时对象复制到正在创建的对象。
int i(10); int i = 10; string str(5,"a"); //直接调用构造函数 string str = "aaaaa"; //先构造一个临时对象,再调用复制构造函数赋值。当情况许可时,可以允许编译器跳过复制构造函数直接创建对象,但编译器没有义务这样做。
15、禁用或改写默认的复制构造函数:
class Father { private: //为了防止复制,必须显式声明其复制构造函数为 private。然而,类的友元和成员仍可以进行复制。如果想要连友元和成员中的复制也禁止,就可以声明一个(private)复制构造函数但不对其定义。
Father(const Father& f){ i = f.i; j = f.j;}; //这里就重现了默认复制构造函数的功能。该构造函数通常不设置为 explicit public: Father(int _i,int _j):i(_i),j(_j){} public: int i,j; }; int main() { Father f1(10,20); Father f2 = f1; //这里其实还用到了重载操作符=,再调用复制构造函数。 效果等同于:Father f2(f1); 但一般效率比后者高 //由于对象禁止复制,所以这里会报错 cout<<"i:"<<f2.i<<" j:"<<f2.j<<endl; }
不允许复制的类对象只能作为引用传递给函数或从函数返回,它们也不能用作容器的元素。
16、即使我们编写了自己的析构函数,合成析构函数仍然运行。如果类需要显式编写析构函数,则它也需要赋值操作符和复制构造函数,这是一个有用的经验法则。这个规则常称为三法则,指的是如果需要析构函数,则需要所有这三个复制控制成员。如果类没有指针成员的话,则一般不需要析构函数,使用默认的复制构造函数和operator=即可满足要求;如果类中有指针成员的话,则一般需要析构函数,并且需要自己写复制构造函数和operator=。默认的复制构造函数是浅复制,直接复制指针,导致一个对象销毁后,另一个对象的指针成员成为悬垂指针。
17、包含指针的类需要特别注意复制控制,原因是复制指针时只复制指针中的地址,而不会复制指针指向的对象。当两个指针指向同一对象时,可能使用一个指针删除了一对象时,另一指针的用户还认为基础对象仍然存在,从而造成悬垂指针。可以定义所谓智能指针的类来解决这一问题,其通用技术是采用一个使用计数。智能指针类将一个计数器与类指向的对象相关联。使用计数跟踪该类有多少个对象共享同一指针。使用计数为 0 时,删除对象。使用计数有时也称为引用计数。
class Use_Count_Father { friend class Father; Use_Count_Father(int *p_):p(p_),use(1){} ~Use_Count_Father(){delete p;} size_t use; int *p; //把 Father 类中的指针成员放到这个类中间接访问。
}; class Father { public: Father(int* p_):ucf(new Use_Count_Father(p_)){} ~Father(){if(--ucf->use == 0) delete ucf;} Father(const Father& f):ucf(f.ucf){++ucf->use; } Father operator=(const Father&); private: Use_Count_Father *ucf; }; int main() { int i = 10; Father f1(&i); //在调用 Use_Count_Father 析构函数中的 delete p; 时会出错,不知道为什么。 }
18、处理指针成员的另一个完全不同的方法,是给指针成员提供值语义。复制值型对象时,会分配另一块内存。string 类是值型类的一个例子。其实就是深复制。
19、可重载的操作符:
+ |
- |
* |
/ |
% |
^ |
& |
| |
~ |
! |
, |
= |
< |
> |
<= |
>= |
++ |
-- |
<< |
>> |
== |
!= |
&& |
|| |
+= |
-= |
/= |
%= |
^= |
&= |
|= |
*= |
<<= |
>>= |
[] |
() |
-> |
->* |
new |
new [] |
delete |
delete [] |
不能重载的操作符:
:: |
.* |
. |
?: |
还有 new、delete、sizeof、typeid、static_cast、const_cast、synamic_cast、reinterpret_cast
20、重载操作符必须具有至少一个类类型或枚举类型的操作数。这条规则强制重载操作符不能重新定义用于内置类型对象的操作符的含义。
21、使用重载操作符而不是创造命名操作,可以令程序更自然、更直观,而滥用操作符重载使得我们的类难以理解。
class Father { public: Father(int i_):i(i_){} int operator+(Father &f); friend ostream& operator<<(ostream &os,Father &f); private: int i; }; int Father::operator+(Father &f) { return i + f.i; } ostream& operator<<(ostream &os,Father &f) { os<<f.i; return os; } int main() { Father f1(13); Father f2(7); cout<<f1+f2<<endl; //20 cout<<f1<<"\t"<<f2<<endl; //13 7 }
22、关于内置类型的自动初始化。
1 #include <iostream> 2 #include <string> 3 4 using namespace std; 5 6 class Test 7 { 8 public: 9 int get_j(){return j;} 10 private: 11 int j; 12 }; 13 int i1; 14 int array1[3]; 15 string str1; //引用类型,会调用默认构造函数的 16 int main() 17 { 18 int i2; 19 int array2[3]; 20 string str2; 21 cout<<"i1:"<<i1<<endl; //0 22 cout<<"i2:"<<i2<<endl; //不确定 23 cout<<"---------------------"<<endl; 24 cout<<"array1[1]:"<<array1[1]<<endl; //0 25 cout<<"array2[2]:"<<array2[2]<<endl; //不确定 26 cout<<"---------------------"<<endl; 27 cout<<"str1:"<<str1<<endl; //"" 28 cout<<"str2:"<<str2<<endl; //"" 29 cout<<"---------------------"<<endl; 30 Test test; 31 cout<<"j:"<<test.get_j()<<endl; //不确定 32 }
23、调用操作符(函数对象)和转换操作符:
#include <iostream> using namespace std; class Test { public: void operator()(int i) { cout<<"operator()"<<endl; } operator int() const { cout<<"operator int()"<<endl; return id; } int id; }; int main() { Test test; test(3); //函数对象 test.id = 10; cout<<test<<endl; //类型重载 }
定义了调用操作符的类,其对象通常称为函数对象,即它们是行为类似函数的对象。