C++ 面向对象高级设计
inline关键字
- 类声明内定义的函数,自动成为
inline函数,类声明外定义的函数,需要加上inline关键字才能成为inline函数
常规函数与内联函数
- 常规函数与内联函数之间的区别不在于编写方式,而在于C++编译器如何将他们组合到程序当中。
程序的运行可以理解为机器指令的逐步运行,当运行函数的时候,立刻保存指令的内存地址,跳到指定函数,并在结束时跳转到地址被保存的指令处,来回跳跃并记录跳跃位置意味着使用函数时,需要一定的开销。而内联函数的编译代码与其他的程序代码“内联”了起来,也就是说,编译器将使用相应的函数代替函数调用,对于内联函数,程序无需跳到另一个位置执行代码,然后再跳回来,因此,内联函数的运行速度比常规函数快,但代价是消耗更多的内存。当一个程序有10个不同的位置调用同一个内联函数的时候,则该程序包含该函数的10个副本,所以应该有选择的使用内联函数,条件如下:
- 如果函数体代码的执行时间较长,则函数调用机制占用的时间可以忽略。
- 如果函数体代码的执行时间较短,则使用内联函数可以节省大部分时间
构造函数
应该使用列表初始化
class complex {
public:
complex(double r = 0, double i = 0) : re(r), im(i){} //列表初始化
private:
double re, im;
};
class complex {
public:
complex(double r = 0, double i = 0){ re = r; im = i; } //赋值构造函数
private:
double re, im;
};
列表初始化更快,是理想的初始化方式。
常量成员函数
如果函数内不改变成员函数的值,则可以加上const修饰,更加的保险。
class complex {
public:
...
double real() const { return re; }
double imag() const { return im; }
private:
double re, im;
};
- 常量对象只能调用常量函数,上述成员函数如果不加const, 那complex类型的常量对象则不能调用。
引用变量
- 引用是已定义的变量的别名
- 引用变量的主要用途是用作函数的形参
- 通过将引用用作参数,函数将使用原始数据,而不是其副本
- 必须在声明引用时将其初始化,而不能像指针那样,先声明,再赋值。
- 一旦与某个变量关联起来,就将一直效忠于它。
参数的值传递和引用传递
应尽量使用参数的引用传递,避免了参数的复制且提高了效率。
返回值的值传递和引用传递
如果返回值是一个临时对象,那应该使用值传递,因为,一旦离开函数体,内部的临时对象就没了,相反,如果传回的对象是传入的对象的话,就使用引用传递,
ostream对象
对于重载的ostream对象,一般返回值的参数是ostream&, 不是void&, 因为我们输出的形式一般是cout<<t1<<t2, 连续输出, 返回ostream& 对象,可以继续输出。
友元
友元函数不属于对象函数,所以不受public,private,protected的限制, 但可以随意访问类内的所有成员和函数。
同一个类内的各个对象互为友元,所以在类定义中可以随意访问其他对象的私有成员, 如下:
class complex {
public:
....
int func(const complex& parm) {
return parm.re + parm.im;
}
private:
double re, im;
};
complex c1, c2;
c1.func(c2);
操作符重载
有两种方式实现操作符的重载:1. 在类内声明public类型的函数实现成员函数重载。2. 在类外声明全局函数重载。
类内声明的操作符重载 +=
在类内声明的操作符重载一般是作用在左操作数上的, 例如:
class complex {
public:
.....
friend complex& __dopal(complex*, const complex&);
complex& operator += (const complex& parm) {
return __dopal(this, parm);
}
private:
double re, im;
};
inline complex& __dopal(complex* ths, const complex& r) {
ths->re += r.re;
ths->im += r.im;
return *ths;
}
complex c1(1, 2);
c1 += 1; //重载 +=
对于类内重载的函数,其内隐含着一个this指针,可以像下面这样理解。
complex& operator += (complex* this, const complex& parm) {
return __dopal(this, parm);
}
- 注意重载的操作符 += 其返回的参数类型为complex&, 是因为可能不止一个两个对象相加,可能多个连加: c1 += c2 += c3;
- 注意不会改动的值或者对象尽量加 const 修饰。
在类外声明的操作符重载 +
类外声明的操作符重载,参数不局限于作用在左操作数,考虑左右操作数的不同形式,所以使用类外重载操作符。
complex c1(2,1);
complex c2;
c2 = c1 + c2; // 用法1: complex + complex
c2 = c1 + 5; // 用法2: complex + double
c2 = 7 + c1; // 用法3: double + complex
在类外声明函数重载 <<
与重载+的考虑方法类似,<<操作符通常的使用方式是cout<<c1而非c1<<cout,因此不能使用成员函数重载<<运算符.
考虑到形如cout<<c1<<c2<<c3的级联用法,重载函数的返回值为ostream&而非void&.
特殊函数
类中有三个特殊的函数:拷贝构造函数, 拷贝赋值函数,析构函数。
对于不带指针的类,这三个特殊的函数一般都使用默认版本,对于带指针的类,那就有必要自己定义这三个函数。
构造函数
构造函数有两种:深拷贝和浅拷贝。
主要区别:
- 深拷贝会在堆内存中另外申请空间来储存数据
- 浅拷贝则是实现类中成员的一一复制
- 当执行两个类之间赋值的时候,在默认版本下,类中执行的都是浅拷贝,当类成员中没有指针的时候,执行浅拷贝是没有问题的,但是当有指针成员的时候,如果执行这样会造成一个现象,那就是两个指针指向同一个对象,等到结束对象使用的时候,会执行两次析构函数,从而造成指针悬挂现象,所以这种情况必须使用深拷贝。
执行浅拷贝解决了指针悬挂问题,当类中有指针成员的时候,必须使用深拷贝。
析构函数
当类中有指针类型成员的时候,在构造函数中一般都使用了new或者new...[]来申请内存,所以当对象使用完毕调用析构函数的时候需要调用delete 指针成员或者delete[] 指针成员来释放内存。切记new 和 delete是相对的,有new就肯定有delete。
拷贝构造函数和拷贝赋值函数
拷贝构造函数和拷贝赋值函数函数的使用场景不同,下面程序的拷贝3虽然使用了=赋值,但是因为是在初始化过程中使用的,所以调用的是拷贝构造函数.
String s1 = "hello";
String s2(s1); // 拷贝1: 调用拷贝构造函数
String s3;
s3 = s1; // 拷贝2: 调用拷贝赋值函数
String s4 = s1; // 拷贝3: 调用拷贝构造函数
拷贝赋值函数中要检测自我赋值,这不仅是为了效率考虑,也是为了防止出现bug.
inline String& String::operator = (const String& str) {
if(this == &str) { //检测自我赋值
return *this;
}
delete[] m_data;
m_data = new char[strlen(str.m_data) + 1];
strcpy(m_data, str.m_data);
return *this;
}
堆栈与内存管理
堆栈及对象的生命周期
栈(stack),在函数中声明的任何局部变量(非静态)都是在栈中分配的(编译期间完成)。并且函数的参数,以及返回值也是依赖于栈。
堆(heap),即动态存储区,与栈不同,堆是在程序运行时被分配的。C语言中的malloc,C++中的new完成的都是堆上的操作。堆不会自动释放所以需要free和delete。
class complex{...};
...
{
complex c1(1, 2); // 所占空间来自于栈(stack)
complex *p = new complex(3); //new分配的,控件来自于堆(heap)
}
- 栈(stack)对象的生命周期:函数体结束之后,堆对象会被自动清理。
- static对象,函数结束之后不会被清理,直到程序结束才会被清理。
- 全局对象是直到程序结束才会被清理
- 堆(heap)对象(new 申请空间定义的对象),会在delete ..释放对象之后被清理

浙公网安备 33010602011771号