@[TOC]
0 背景
因为本人的C++基础不是很扎实,很多面向对象的基本语法掌握的不是很熟练,导致写程序出错时,经常会犯错,于是就去网上找到了侯捷老师的C++课进行观摩学习。
本文是在观看完侯捷老师的《面向对象高级开发》课程后一些笔记和心得。文中老师一直强调要养成写C++大气、正规、高效的编程方法,实际就是要培养良好的编程习惯。
1 防御式编程
例如在定义头文件时,都使用
#ifndef
#define
//...
#endif
为的就是防止头文件被重复包含。
2 内联函数(inline)
inline:在类内定义的函数,都默认为内联函数,类外的函数,则需要在返回类型前添加inline。
inline只是建议编译构建函数时,构建为内联的,具体是不是则由编译决定。
inline作用:空间换时间,加快程序的运行速度。
3 构造函数(constructor)
一般使用初始化列表(initialization list)来初始化参数。
没有在初始化列表中的成员参数会被隐式初始化
构造函数一般是声明为public,供他人创建,也有声明为private的,例如单例模式。
class A{
public:
A(int _data):m_data(_data){}
private:
int m_data;
};
单例模式:
class{
public:
static A& getInstance(){//调用函数时,才创建对象
static A a;
return a;
}
private:
A(){}
};
3.1 必须使用初始化列表
- 1,成员是引用数据成员,常量数据成员和对象数据成员(可能没有赋值运算符函数)时,不能被赋值,只能被初始化
- 2,提高效率:初始化列表只会调用一次构造函数,在使用函数体内初始化时,一般会同时调用构造函数和复制操作符函数,重复的函数调用是浪费资源的。
示例:
#include
int g_x = 6;
class A{
public:
// A(){//错误构造函数
// m_data = 5;
// m_data2 = g_x;
// }
A(int x):m_data(x),m_data2(g_x){//正确的构造函数
}
private:
const int m_data;
int& m_data2;
};
int main(){
return 0;
}
3.2 无法使用初始化列表
- 1,成员变量相互依赖,要使用一个类成员初始化另一个类成员。
例如:
#include
class A{
public:
A(int _data):m_data1(data2),m_data2(_data){}
private:
int m_data1, m_data2;
};
int main(){
return 0;
}
3.3 默认构造函数
不带任何参数的构造函数。
3.3.1 默认实参(default argument)
下面的代码会引起歧义:
因为默认构造函数和带默认实参的构造函数等价,编译器调用时,不知道调用哪一个
#include
class A{
public:
A(int _data = 0):m_data(_data){}//默认实参 default argument
A(){}//默认构造
private:
int m_data;
};
int main(){
A a;//编译不通过
return 0;
}
使用默认实参的注意事项:
1,函数声明时,参数必须按照从右向左的顺序,依次给与默认值(也就是默认实参必须是依次从右到左的,不能跳参数);
例如:int f(int a = 1, int b, int c = 3);//错误 int f(int a, int b = 2, int c );//错误
- 2,函数调用传参时,必须按照从左向右的顺序,依次赋值;
例如:
f(,2 ,3);//错误
f(1,,3);//错误
4 类
类中数据一般都使用private,来封装数据,防止他人随意更改。
如果需要提供读取功能,使用存取器函数,例如下面的getData函数。
class A{
public:
int getData() const{return m_data;}
private:
int m_data;
};
5 常量
5.1 常量成员函数
格式(下面的const):
return-type functionName(param ) const{}
作用:表示该函数不会改变成员变量的值。
例如下面的例子,常量对象a只能调用常量函数,如果调用非常量函数,就意味着可能会改变对象中成员的值,因为会报错。
#include
class A{
public:
A(int _data = 0):m_data(_data){}
int print(){return m_data;}
int print2() const { return m_data;}
private:
int m_data;
};
int main(){
const A a(6);
// a.print(); //错误的用法
a.print2();
return 0;
}
5.2 常量/非常量对象和函数
类型 | const object | non-const object |
---|---|---|
const成员函数 | true | false |
no-const成员函数 | false | true |
函数属性加了const后,就是两个两个不同函数(函数签名不同,不考虑返回类型)。
例如:
char operator[](size_type pos) const{}
reference operator[](size_type pos){}
当成员函数的const和non-const版本同时存在时,const object只会调用const版本,non-const object只会调用non-const版本
6 参数传递
两种形式:pass by value(值传递)/pass by reference(引用传递)
- 1,建议能传引用的尽量都传引用(底层使用指针实现【32位或64位都是4个字节大小】),因此传递时比其他大部分变量都快(除了单个字符【一个字节】);
- 2,不能使用引用传递的情况,当传递的值位局部变量时,无法传递引用;
例如:
#include
class A{
public:
A(int _data = 0):m_data(_data){}//默认实参 default argument
A& operator++(){//前++,可以作为左值
this->m_data++;
return *this;
}
A operator++(int){//后++,因为为临时对象,无法作为左值
A tempA(m_data);
m_data++;
return tempA;
}
void getData()const { std::cout<
- 3,当传递的值,在原类中已存在,就可以传递引用,如上面的
A& operator++()
函数。 4,传递者无需知道接受者是否以引用的方式接收。
例如:inline A& (){//返回类型为接受者 //... return *this;//传递者 }
7 友元
赋予其他函数或者类访问类内部protected
或者private
成员的访问权限,打破了类的封装。
相同类class的各object互为友元。
8 操作符重载(operate overloading)
分类:成员函数/非成员函数。
区别:只有非静态成员函数中有this指针。
使用非成员函数的情况:
- 1,符合以往的编程习惯;
例如,如果使用内部操作符函数,则会造成使用方式与以往不同的情况:
因为不能更改已有cout的成员操作符号函数,因此只能创建一个非成员函数。
#include
#include
class A{
public:
A(int _data = 0):m_data(_data){}//默认实参 default argument
std::ostream&operator<<(std::ostream& os){
os<m_data;
return os;
}
void printData()const { std::cout<
9 临时对象(local object)
创建的临时对象,到下一行时,就会自动销毁,不能返回引用。
10 Big Three(三位一体原则)
“三位一体原则”:如果类需要一个析构函数,那它同时可能也需要一个拷贝构造函数和一个赋值运算符成员函数。
测试例子:
#include
#include
class A{
private:
char* m_data;
public:
A ():m_data(nullptr){}//默认构造
A (const char* _data){//实参构造
//(不使用初始化列表,是是因为初始化列表不能给m_data分配内存,只会让两个指针指向同一块地址)
this->m_data = new char[sizeof(_data)];
std::strcpy(this->m_data, _data);
}
//实参构造,当_data是常量时,不会分配内存,会导致delete析构函数报错
//只会拷贝指针,使得两个指针指向同一对象,推荐使用在函数体内初始化
// A (char* _data):m_data(_data){ }//带初始化列表的构造函数
//推荐,来代替上面的方法
A(char* _data){//实参构造
this->m_data = new char[sizeof(_data)];
std::strcpy(this->m_data, _data);
}
A (const A& a){//拷贝构造
this->m_data = new char[sizeof(a.m_data)];
std::strcpy(this->m_data, a.getData());
}
A&operator=(const A& a){//赋值运算符函数
if(&a != this){
delete[] this->m_data;
this->m_data = new char[sizeof(a.getData())];
std::strcpy(this->m_data, a.getData());
}
return *this;
}
~A(){//析构函数
std::cout<<"调用析构函数"<m_data;
}
char* getData() const { return m_data;}
void getDataAddress() const {std::cout<<&(m_data)<m_data<
在写管理资源(如内存资源)的类时,如果没有定义析构函数,默认析构函数将会被调用。这个默认析构函数会仅删除指向对象的指针,而删除一个指针不会释放指针指向对象占用的内存,最终会导致内存泄露。
如果只提供一个析构函数,而不显示写出复制构造函数与赋值运算符函数,那情况可能更糟糕。如果调用默认的构造函数,进行的是指针传递而不是值传递,导致两个对象共享一个内存空间,当其中一个对象被删除后,析构函数将释放那片共享的内存空间,接下来对这片已经释放了内存的任何引用都将会导致不可遇见的后果。
进行赋值操作符函数编写时,要进行自我赋值判断,如果不是自我赋值,才删除原对象并释放内存,然后复制新对象。
如果去掉这个判断,会造成将左操作数对象的元素删除并释放其占用的内存的同时,由于左右操作数指向同一对象,导致右操作数同时被删除。但还要将右操作对象复制,这将会带来灾难。
例如:
template
Vec& Vec::operator=(const Vec& rhs){
if(&rhs != this){//进行字符赋值判断
uncreate();//删除运算符左测的数组
create(rhs.begin(), rhs.end());//从右侧元素复制到左侧
}
return *this;
}
11 生命范围
在栈中:
- 1,生命域在作用域({ })中的auto object(带有析构函数),作用域结束,会自动调用构造函数;
- 全局数据段:
- 1,static object(被放在)在作用域结束后仍存在,直到程序结束;
- 2,在作用域外,也就是全局对象
堆:
- 1,使用new/new[]创建的对象(注意:一定要配合delete/delete[]使用),因为不会自动销毁(如果是父类,一般析构函数要声明为虚函数,为了方便子类在多态时调用)
12 编译器下的new和delete内幕
12.1 技术内幕
例子:
#include
#include
class A{
public:
A(){std::cout<<"constructor:"<(this)<(this)<
程序输出:
new
constructor:0x7fe643c05960
destory:0x7fe643c05960
delete
new[]
constructor:0x7fe643c05978
constructor:0x7fe643c0597c
constructor:0x7fe643c05980
destory:0x7fe643c05980
destory:0x7fe643c0597c
destory:0x7fe643c05978
delete[]
如上所示,在调用A* a = new A;
时,编译器实际执行的操作为:
1,分配内存(调用operator new函数)
void* tempP = operator new(sizeof(A));
- 2,转型
a = static_cast(A*)(tempP);
- 3,构造函数
a->A::A();
在调用delete a
时,编译器实际进行的操作为:
1,调用析构函数
a->A::~A();
- 2,释放内存(调用operator delete 函数)
当使用new[]和delete[]时,构造和析构的顺序相反。
13 静态成员变量/函数+静态全局/局部变量
静态的作用:
- 1,延长生命周期(局部变量)
- 2,都默认初始化为0(全局或局部修饰的变量)
- 3,隐藏功能(只对同一个源文件有效【同名的.h、.cpp】,避免命名冲突)
13.1 静态成员变量/函数
- 1,静态成员变量:同一个类中只有一份,多个类共用一个;
- 2,静态成员函数:函数内没有this指针,只能改变静态变量的值(因为静态成员函数属于整个类,在类实例化对象之前就已经分配空间了,而类的非静态成员必须在类实例化对象后才有内存空间),用于配合静态变量使用;静态成员函数中不能调用非静态函数,反之则可以。
#include
#include
class A{
public:
A(){}
static void func(){
m_data = 2;
//m_data2 = 3;//error: invalid use of member 'a' in static member function
}
void print(){
std::cout<func();//静态函数
a.print();
a.func();//通过对象名调用
a.print();
return 0;
}
13. 2 非成员静态全局/局部变量
- 1,静态全局变量:静态全局变量在头文件中可以声明也可以定义,只对同一个源文件(同名的.h、.cpp)有效,避免命名冲突;但是extern变量在头文件中只能声明,然后在源文件(.cpp)中必须有定义(因为extern用于变量,表示它在其他地方定义,在运行前就已经存在)。
- 2,静态局部变量:将函数中此变量的值保存至下一次调用时,只能被初始化一次(用于避免使用全局变量时,破坏了此变量的访问范围)
test.h
#ifndef ALGORITHM_TEST_H
#define ALGORITHM_TEST_H
static double g_grade;//不初始化时,默认为0
extern double g_grade2;
//double g_grade3;//错误:ld: 1 duplicate symbol for architecture x86_64
void func2();
#endif //ALGORITHM_TEST_H
test.cpp
#include "test.h"
#include
double g_grade2 = 100;
void func2(){
//全局静态变量
//打印默认值
std::cout<<"test.cpp--->g_grade全局静态变量赋值前:"<g_grade全局静态变量赋值后:"<g_grade2全局变量:"<
main.cpp
#include
#include
#include "test.h"
int func1(){//静态局部变量
static int n = 0;
n++;
return n;
}
int main(){
//局部静态变量
std::cout<<"局部静态变量:";
for(int i = 0;i < 5;++i){
std::cout<g_grade全局静态变量赋值前:"<g_grade2全局变量赋值前:"<g_grade全局静态变量赋值后:"<g_grade2全局变量赋值后:"<
输出:
局部静态变量:1 2 3 4 5
全局变量:
test.cpp--->g_grade全局静态变量赋值前:0
test.cpp--->g_grade全局静态变量赋值后:92
test.cpp--->g_grade2全局变量:100
main.cpp--->g_grade全局静态变量赋值前:0
main.cpp--->g_grade2全局变量赋值前:100
main.cpp--->g_grade全局静态变量赋值后:96.8
main.cpp--->g_grade2全局变量赋值后:96.8
13.3 静态非成员函数
- 1,作用:声明的函数定义时,只在当前文件附属的.cpp中有效,在其他地方可以重复定义。
test.h
#ifndef ALGORITHM_TEST_H
#define ALGORITHM_TEST_H
void func2();
static void func3();
#endif //ALGORITHM_TEST_H
test.cpp
#include "test.h"
#include
void func2(){
}
void func3() {
std::cout<<"test.cpp--->全局静态函数:func3()";
}
main.cpp
#include
#include
#include "test.h"
//void func2(){//错误:ld: 1 duplicate symbol for architecture x86_64
//
//}
//可以在这里重复定义func3
void func3(){
std::cout<<"main.cpp--->全局静态函数:func3()";
}
int main(){
func3();//要调用此函数,main.cpp中必须定义func3,其他源文件中定义无效
return 0;
}
14 命名空间
14.1 意义和用法
为了区分不同库中相同名称的函数、类、变量,使用了命名空间即定义了上下文。
#include
namespace first_space{
void func() {
std::cout << "first_space:func()"<
14.2 使用标准库
- 1,using directive
eg:using namespace std;
- 2,using declaration
eg:using std::cout;
15 三大面向对象关系(复合、委托、继承)
15.1 composition(复合)
- 1,构造关系:由内到外
- 2,析构关系:由外到内
例如:
#include
class Son{
public:
Son(){std::cout<<"Son construction"<
输出:
Son construction
Mother construction
Mother destroy
Son destroy
15.2 Delegation(委托)【composition by reference】
一种通过引用的特殊的复合
Pimpl(pointer to implementation):母类只是对外的接口,真正的实现都在子类里,当母类需要动作的时候,就去调用子类。
作用:母类有一个指针去指向实现所有功能的子类,这种手法的好处在于,这个指针还可以去指向不同的实现类,去实现不同的功能,子类不管怎么变动都不影响s母类也就不影响客户端,母类也永远不用再编译。
示例:
#include
class Mother;
class Son{//真正的实现
friend class Mother;
public:
Son(int data = 0):m_data(data){std::cout<<"Son construction"<printData();}
void setData(int data){son->setData(data);}
private:
Son* son;//Pimpl(pointer to implementation)
};
int main(){
Mother mother;
mother.setData(2);
mother.printData();
return 0;
}
输出:
Mother construction
data:2
Mother destroy
15.3 Inheritance(继承)
表示:is-a
调用函数时,是在运行时决定,而不是在编译期间决定。
公有继承:所有公有成员和保护成员保持原装
私有继承:所有公有成员和保护成员都成为派生类的私有成员
保护继承:所有公有成员和保护成员都成为派生类的保护成员
15.3.1 成员函数类型
- 1,non-virtual函数:不希望被derived(派生类)override(重新定义); 【void fun();】
- 2,virtual函数:希望被derived类重新定义,且已有默认值;【virtual void fun();】
- 3,pure函数:一定要重新定义【virtual void fun() = 0;】
15.3.2 使用条件以及示例
条件:
- 1,函数时虚函数,声明时用virtual修饰;
- 2,使用指针或引用调用;
- 3,指针或引用的对象必须是基类。
纯函数: 函数变量列表后加 = 0
声明了纯虚函数的类,不能实例化为对象。必须要实现后,才能实例化。
构造顺序:基类---->派生类
析构顺序:派生类--->基类
范例:
#include
class Base{
protected:
//纯虚函数:无法实例化
virtual void printData()const = 0;
};
class Father:public Base{//对外接口
public:
Father(int data = 0):m_data(data){std::cout<<"Father construction"<"<print();
sonP.print();
//显示调用
std::cout<<"显示调用------------->"<
输出:
Father construction
Father construction
Son construction
Father print()
Son print()
动态绑定------------->
Father print()
Son print()
显示调用------------->
Father print()
Son destroy
Father destroy
Father destroy
15.3.3 template Mothod设计模式【模板方法模式】
作用:在父类中定义处理流程的框架,在子类中实现具体处理。
MFC中经常用到此方法。
#include
using std::cout;
class Base{
private:
virtual void calculate() = 0;
public:
void getResult(){
std::cout<<"satrt:"<
输出:
satrt:
calculate
end
15.4 继承+复合
构造顺序:A--->C--->B
析构顺序:B--->C--->A
//计算复合、继承的构造函数先后
#include
using std::cout;
using std::endl;
class A{
public:
A(){cout<<"构造A "<
输出:
构造A
构造C
构造B
析构B
析构C
析构A
15.5 继承 + 委托
15.5.1 案列
解决的问题:一个文件,四个窗口,当文件变化时,窗口也会跟着改变。
设计方法:
#include
#include
//前置声明
class Observer;
//数据
class Subject{
private:
int m_value;
std::vector m_views;
public:
void attach(Observer* objs){
m_views.push_back(objs);
}
void setVal(int value){
m_value = value;
notify();
}
//直接在类内定义会报错:error: member access into incomplete type
void notify();
};
//视图
class Observer{
public:
virtual void update(Subject* subject, int value) = 0;
};
void Subject::notify() {
for(int i = 0;i < m_views.size();++i){
m_views[i]->update(this, m_value);
}
}
int main( ){
return 0;
}
15.5.2 Composite设计模式【部分整体模式】
解决问题:文件系统,里面有文件夹或文件
//文件系统
#include
#include
class Component{
public:
Component(int data):m_data(data){}
virtual void add(Component*){}
private:
int m_data;
};
//文件
class Primitive:public Component{
public:
Primitive(int data):Component(data){}
};
//文件夹
class Composite:public Component{
public:
Composite(int data):Component(data){}
void add(Component* elem){m_p.push_back(elem);}
private:
std::vector m_p;
};
int main( ){
return 0;
}
15.5.3 Prototype设计模式【原型模式】
作用:用于创建重复的对象,同时又能保证性能。
解决问题:创建未来才会出现的子类,让子类自己创建自己(原型),然后基类可以看见原型并复制它。
#include
#include
enum imageType{
LSAT,SPOT
};
class Image{
public:
//发现和复制
static Image* findAndClone(imageType);
virtual void draw() = 0;
static void printNextSlot(){std::cout<<"m_nextSlot:"<returnType() == type)
return m_protoTypes[i]->clone();
}
int main( ){
Image* landSatImage = Image::findAndClone(LSAT);
Image* spotImage = Image::findAndClone(SPOT);
Image::printNextSlot();
std::cout<<"type:"<returnType()<
16 类型转换
16.1 conversion function(类型转换函数【无返回类型】)---->将该类转为其他类型
下面的例子是通过类型转换函数,将f转为double类型。
//测试类型转换
#include
class Franction{
public:
explicit Franction(int num, int den = 1):m_numerator(num),m_denominator(den){}
//Franction(int num, int den = 1):m_numerator(num),m_denominator(den){}
// Franction operator +(const Franction& f){ return Franction(this->m_numerator + f.m_numerator,this->m_denominator + f.m_denominator);}
//类型转换函数
operator double(){ return static_cast(m_denominator/m_denominator);}
private:
int m_numerator;
int m_denominator;
};
int main(){
Franction f(3,5);
//Franction d2 = f + 4;
double d2 = 4 + f;
return 0;
}
16.2 explicit(显示构造声明)---->构造函数+操作符号函数:将其他类型转为该类(隐士构造转换)
下面的例子是将4用构造函数转为Fraction类型,但是如果构造函数使用explicit声明后,将无法隐式转换。
explicit作用:构造函数只是在显示声明时使用,无法进行隐士转换,比如把4自动转为Fraction类型。
//测试类型转换
#include
class Franction{
public:
//使用explicit后,不能进行隐士转换
//explicit Franction(int num, int den = 1):m_numerator(num),m_denominator(den){}
Franction(int num, int den = 1):m_numerator(num),m_denominator(den){}
//操作符号函数
Franction operator +(const Franction& f){ return Franction(this->m_numerator + f.m_numerator,this->m_denominator + f.m_denominator);}
//类型转换函数
// operator double(){ return static_cast(m_denominator/m_denominator);}
private:
int m_numerator;
int m_denominator;
};
int main(){
Franction f(3,5);
Franction d2 = f + 4;//使用explicit声明构造函数后会报错
//double d2 = 4 + f;
return 0;
}
16.3 Ambiguous歧义
转换路线:
- 1,f使用operator double()转为double,然后与4相加后,再通过构造函数转为Franction;
- 2,4使用构造函数转为Franction,然后与f相加
//测试类型转换
#include
class Franction{
public:
//使用explicit后,不能进行隐士转换
//explicit Franction(int num, int den = 1):m_numerator(num),m_denominator(den){}
Franction(int num, int den = 1):m_numerator(num),m_denominator(den){}
//操作符号函数
Franction operator +(const Franction& f){ return Franction(this->m_numerator + f.m_numerator,this->m_denominator + f.m_denominator);}
//类型转换函数
operator double(){ return static_cast(m_denominator/m_denominator);}
private:
int m_numerator;
int m_denominator;
};
int main(){
Franction f(3,5);
Franction d2 = f + 4;//error: use of overloaded operator '+' is ambiguous
//double d2 = 4 + f;
return 0;
}
如果构造函数声明为explicit,那将会报错 error: no viable conversion from 'double' to 'Franction'
,因为对于第一种情况,无法将最终结果转为Franction;对于第二种情况,无法将4转为Franction,因此都将无法完成操作。
17 开始编写代码前的设计
1,考虑传入的参数时否可以是&(左值),是否用const(不会修改传入的值);
- 传入&类型参数:只能是左值 - 传入const &类型参数:可是左值或者右值(临时对象) - &类型参数不能是右值的原因:传&的意图是改变对象值,但是传递右值时,编译器会生成一个临时匿名对象,让&类型参数指向它(这些临时匿名对象只会在函数调用期间存在,随后编译器便将它删除),因此不能做到更改值的意图,C++为了阻止这种情况发生,便在这种情况下禁止创建临时变量; - const &类型参数可以是右值的原因:const &本就意味着不会对对象造成更改,因此C++会为其创建临时匿名对象(只在函数调用期间存在)
- 2,考虑返回的类型是否是&(对象要不是loacl object【局部对象】),函数类型是否用const(函数不改变类成员变量值)
#include
#include
double func(const double& ra)
{
return ra*ra;
}
double func2(double& ra){
return ra*ra;
}
int main(){
//左值
double side = 3.0;
double* pd = &side;
double& rd = side;
//右值
long edge = 5L;
double lens[4]={2.3,3.4,4.5,6.7};
//const&类型
//左值参数
double c1 = func(side); // ra 是side
double c2 = func(lens[2]); // ra是lens[2]
double c3 = func(rd); // ra 是 rd
double c4 = func(*pd); // ra 是*pd
//右值参数
double c5 = func(edge); // ra 是临时变量(double引用不能指向long)
double c6 = func(7.0); // ra 是临时变量
std::cout<<"c5:"<
18 不同类
18.1 pointer-like class(仿指针)
重载*、->运算符函数。
#include
#include
template
class Share_ptr{
public:
Share_ptr(T* p):px(p){}
T& operator*() const{ return *px;}
T* operator->() const{return px;}
private:
T* px;
};
class A{
};
int main(){
Share_ptr sp(new A);
A a(*sp);
return 0;
}
18.2 function-like class(仿函数)
重载函数调用操作符
#include
#include
template
struct identity{
const T&operator()(const T& x) const{
return x;
}
};
template
struct select1st{
const typename Pair::first_type& operator()(const Pair& x) const{
std::cout<<"operator()";
return x.first;
}
};
int main(){
std::pair aPair;
select1st>()(aPair);
return 0;
}
19 虚函数之虚指针、虚表
继承中的虚函数的继承,就是虚函数的调用权。
下面的表中,例如A中有虚函数,因此类A的内存中,有一个虚指针(vptr),然后虚指针指向虚表(vptr),虚标中存储着函数的地址。