[Cpp] 面向对象程序设计 C++
初始化列表(包括成员对象初始化)
初始化列表 ( 推荐 ) :
可以初始化任何类型的数据, 不管是不是普通类型还是对象,都建议用.
不再需要在构造器中赋值了, 而且初始化列表比构造函数要早执行.
成员初始化次序取决于成员在类中的声明次序.
当类成员有其它对象时,构造器内给对象赋值会触发成员对象的默认构造函数(无参数的),如果成员对象没有默认构造函数编译报错.
所以有成员变量为对象这种场景下,要用 initializer list.
Source:https://github.com/farwish/unix-lab/blob/master/cpp/Initializer_list.cc
继承
复用的一种方式,还有上面介绍过的 "对象组合"(成员变量为其他对象)
私有属性只能由父类自己访问;受保护的属性可以由子类访问,别人都无法访问.
当实例化子类时,会先调用父类的构造函数,当父类没有默认构造函数时又没有初始化自己的构造函数时,编译报类似 "no matching function AA::AA( )",所以在子类中只能用 initializer list 对父类成员初始化.
析构的调用次序则反过来,先子类后父类.
Source: https://github.com/farwish/unix-lab/blob/master/cpp/Extends.cc
函数重载(Function overload)和默认参数(Default argument)
同名函数通过拥有不同的参数表实现重载. void print( ); void print ( int i )
默认参数是在头文件中给原型的默认参数值,唯一的好处是某些情况下少打字;但是在调用时容易造成阅读困难,另外也不安全,如果我们不 include 头文件而是自己写一个函数声明,把默认参数值设为其它的,那么就和设计者的意图不一样。所以建议不使用 Default argument 如 void f (int i , int j = 10);
内联函数(Inline functions)
当函数前面有 inline 时,它就是一个 declaration,而不再是 defination,因此不需要担心重复定义的问题。
内联函数的 body 放在头文件里就可以了,不需要 .cpp 文件,和传统的一个 .h 对应一个 .cpp 不同。
因为内联函数有类型检查,因此比做同样事情的宏要好。
( 使用场合:函数只有2~3行的,需要重复调用的;不适合的:函数比较大,递归 )
成员函数在 class 声明时如果给出了 body,那么这些都是 inline 函数,只要有一个头文件就够了。
另一种写法是保持 class 声明干净,而为单独实现的成员函数前面加 inline.
Source: https://github.com/farwish/unix-lab/blob/master/cpp/Inline.h
Source: https://github.com/farwish/unix-lab/blob/master/cpp/Inline_main.cc
const; 不可修改的对象(对象成员)
成员函数 const 的用法:
在声明和定义的地方要一起用.
int getData( ) const;
int getData( ) const { return data }
不修改数据的成员函数应该被定义为 const.
如果类有 const 成员变量 或者 实例一个 const 对象,那么一定要在 initialize list 里面初始化变量,否则编译无法通过,因为后面无法修改它 (成员变量)。
func( ) { } 和 func( ) const { } 是不一样的,它们构成重载( overload ),因为它们相当于是 func( A* this ) 与 func( const A* this ),参数表不一样。
Source: https://github.com/farwish/unix-lab/blob/master/cpp/Const_class.cc
引用(C++数据类型)
char c; char* p = &c; char& r = c;
本地变量或全局变量,必须有初始值,type& name = 'name'
int x = 3;
int& y = x; # 赋初值
const int& z = x; # z 不能做左值,但是可以通过修改 x 来修改 z
作为参数和成员变量时,可以没有初始值,因为它们会在构造对象时被调用者初始化,type& name;
void f ( int& x )
f ( y ); # 在函数调用时初始化
指针和引用的区别:
引用不能为 null. 指针可以为 null.
引用依赖另一个变量,是一个变量的别名. 指针独立于已存在的对象.
引用不能指向一个新的地址. 指针可以更改指向不同的地址.
cpp内存模型的复杂性体现在:三个地方放对象(堆,栈,全局数据区),访问对象的方式(变量放对象,指针访问,引用访问)。
Source: https://github.com/farwish/unix-lab/blob/master/cpp/Reference.cc
引用再研究
引用作为类的成员时,声明时没有办法给初始值,因为它需要和另外一个变量捆绑在一起,作为别名;所以必须在构造函数的 initializer list 里初始化。
函数可以返回一个引用,但不能引用本地变量。
参数前的 const 的引用,const 保证不被修改,引用使传参高效,好处是函数中不用使用 * 号。
参数传引用,这说明参数是一个可以做左值的东西,传参不能使用变量非const的表达式。
void func(int &); func(i * 3); // error:invalid initialization of non-const reference of type 'int&' from a temporary of type 'int' error: in passing argument 1 of 'void f (int &)'
void func(const int&); int i = 3; func(i * 3); // 区别仅在于参数是const的,正确输出9
不能对函数返回的对象做左值,编译会报错,error: using temporary as lvalue [-fpermissive]。
Source: https://github.com/farwish/unix-lab/blob/master/cpp/Reference_2.cc
向上造型(Upcasting)
子类的对象当做父类的对象来看,叫做向上造型,因为一般习惯把父类画在上面;Upcasting 一定是安全的,最多子类拥有的被无视。
父类的对象当做子类的对象看,叫做向下造型,Downcasting 有风险,因为父类不一定拥有子类的东西。
类型转换和造型的区别,类型转换原来的值转换完就变了,而造型数据没变,子类的对象还是子类的对象,只是看待的眼光不一样。
Persion John('JOHN');
Animal* p = &John; // Upcast, 因为Person是Animal的一种, 但反过来就是 Downcast
Animal& q = John; // Upcast
多态性(polymorphism):Upcast 和 Dynamic binding 两个条件构成多态性
Upcast: 把派生类当做基类使用。
Dynamic binding: 调用对象的函数。
(Static binding: 调用代码写明的函数)
/**
* 通用函数,对任何 Shape 和其子类都通用.
*
* 动态绑定,调用的 render 在运行时决定:
* p 有一个静态类型和动态类型,如果 p 的 render 函数是 virtual 的,那么是动态绑定,不是 virtual 则是静态绑定。
* 所以动态绑定还是静态绑定取决于 render 函数,而不是对象 p;如果我们调用的是 move 函数,那么就是静态绑定。
*/
void render(Shape* p)
{
p->render();
}
Source: https://github.com/farwish/unix-lab/blob/master/cpp/Polymorphism.cc
虚 析构函数
Shape的析构不是 virtual 时,默认是静态绑定,delete p 时,只有 Shape 的析构会被调用,Ellipse 的不会调用。
Shape的析构是 virtual 时,表示动态绑定,delete p 时,会先调子类的析构,在调父类的析构。
Shape* p = new Ellipse(100.0, 110.0);
delete p;
其它 OOP 语言默认就是 virtual 的,也就是动态绑定的,而C++默认是静态绑定的,动态绑定需要手动加 virtual。
如果一个类里有一个 virtual 函数,它的析构函数就必须是 virtual 的。
如果父类和子类有名字相同、参数表相同的 virtual 函数,那么子类成员函数就对父类构成了重写/覆盖。
子类成员函数中调用父类的同名函数用 Base::func( ) 的方式。
父类里有两个 virtual 的重载(overload)函数,那么子类里也要实现两个 overloaded 的函数,否则另一个函数会发生 name hidden,只有 C++ 会发生函数的隐藏。
Source: https://github.com/farwish/unix-lab/blob/master/cpp/Polymorphism.cc
拷贝构造
拷贝构造的唯一形式:T::T(const T&)
拷贝构造什么时候被调用?
1.用对象进行初始化时,Person p = p1 或 Person p(p1),这两种写法相同,注意它不是 assignment 而是 initialization (因为变量前有类型)。
2.调用一个函数,函数的参数是一个对象时,void func(Person p);
3.用返回对象的函数返回值进行初始化。
Construction vs Assignment
每个对象只能构造一次,每个对象应该被析构一次,
对象一旦被构造,它可以是被赋值的目标,前头有类型就是 initialization,没有类型就是 assignment.
Copy constructor guidelines
写一个类就先写三个函数 default constructor, virtual distructor, copy constructor。
如果确实不需要拷贝构造,那么就声明为私有,不建议这么做,限制了很多事不能做。
Source: https://github.com/farwish/unix-lab/blob/master/cpp/Copy_constructor.cc
静态对象
static两个基本的含义:
静态存储,本地变量是static,这个本地变量具有持久存储(事实上static的本地变量就是全局变量)。
名字可见性,全局变量、函数的static,那么这个全局变量、函数只在当前文件中可用。
static 在 C++ 中的使用:
静态本地变量 - 持久存储。
静态成员变量 - 所有对象间共享。
静态成员函数 - 所有对象间共享,它只能访问静态成员变量。
对象是静态的 - 除了遵守两个基本法则(存储、可见性),保证只构造析构一次。
静态初始化的依赖
多个 cpp 文件都有自己的全局变量的情况,没人保证初始化顺序先后;
如果一个变量的初始化依赖另一个变量的值作为参数,那么需要先初始化那另一个变量,但是跨文件的初始化是不存在的。所以解决方案是,1. 别这么干。 2. 逻辑上许可的话,把所有有依赖的全局变量放到一个地方。
Source:https://github.com/farwish/unix-lab/blob/master/cpp/Static_members.cc
运算符重载(Overloading Operators) - 基本规则
运算符允许通过自己定义 function 来重载。
只有已存在的运算符可以被重载,不能对类和枚举重载,必须保持操作数个数(如加法需要两个操作数),必须保证优先级。
使用 operator 关键字作为函数名字,如重载 * 号,就是 operator * (...)
可以作为成员函数,const String String::operator + (const String& that); 需要两个操作数,因为有默认参数 this 作为第一个,所以再只需要一个。
可以是全局函数,const String operator + (const String& r, const String& l); 这时参数表需要两个参数。
Integer x(1), y(5), z;
x + y; ====> x.operator+(y); 这里的 x 就是receiver,receiver 决定 operator 用哪个。
z = x + y; 可以。
z = x + 3; 可以。
z = 3 + y; 编译通不过。
Tips:做成成员函数还是函数?
单目的运算符应该做成是成员的,但非强制。
= ( ) [] -> ->* 必须是成员的。
赋值运算符应该做成是成员的。
所有其它二元操作符作为非成员的。
原型
参数传递:如果是只读的,那么传 const 的“引用”,不修改算子的成员函数加 const,全局函数可能两个都加或者又一个不加 const。
返回值:1.决定了是对自己进行了修改还是返回了新对象; 2.制造出的新对象是不是可以做左值;
运算符原型
Refer:有哪些C加加的使用场景