C++面向对象程序设计(侯捷)学习笔记(一)

 

1. 课程目标

(1)以良好的方式编写C++的class,基于对象(Object Based)--面对的是单一class的设计

  • 不带指针的类(class without pointer members)
  • 带指针的类(class with pointer members)

(2)学习classes之间的关系,面向对象(Object Oriented)--面对的是多重classes的设计,classes和classes之间的关系。

  • 继承
  • 复合
  • 委托

(3)C++学习内容:分为基础语言和标准库的学习

C++面向对象程序设计(侯捷)学习笔记(一)_第1张图片

2. C vs C++,关于数据和函数

C++面向对象程序设计(侯捷)学习笔记(一)_第2张图片

C语言中函数会处理数据,数据拥有不同的类型,因此会产生许多变量。各个函数均可处理根据数据产生的变量。

C++面向对象程序设计(侯捷)学习笔记(一)_第3张图片

C++将数据和处理这些数据的函数包在一起(其他函数不可以处理这些数据),组成class(相当于C中的struct),根据这个class可以产生很多对象。

2. C++代码的基本形式

C++面向对象程序设计(侯捷)学习笔记(一)_第4张图片

包含三部分:

主程序文件.cpp,包含main函数

类的声明,一般定义为.h文件,在主程序文件中用 “” 引入

标准库,在编译环境中带有,引入利用<>,可以是.h文件,也可以无延伸名。

备注:

标准库中,后缀为.h的头文件c++(98年后)标准已经明确提出不支持了。

C++ 是不一样,在编译器的include文件中是两个文件,代码也不同。

当使用时,相当于在c中调用库函数,使用的是全局命名空间,也就是早期的c++实现;

当使用的时候,该头文件没有定义全局命名空间,C++标准程序库中的所有标识符都被定义于一个名为std的namespace中,必须使用namespace std;这样才能正确使用标准库中的标识符。

3. Header的组成

C++面向对象程序设计(侯捷)学习笔记(一)_第5张图片

(1)防卫式说明:防止重复引入,造成重复定义问题。依靠宏的名字,防止宏重名。

#ifndef __COMPLEX__
#define __COMPLEX__
...
#endif

 以下代码若complex.h中,无防卫式声明,下面的代码会报出"./complex.h:7:7: error: redefinition of 'complex'"(重复定义)错误。这是因为,在编译器预编译的阶段,会将.h文件展开成其中的内容,无防卫式声明则会重复定义,造成上面的问题。

#include
#include "complex.h"
#include "complex.h"
using namespace std;
int main() {
    const complex c1(2.0,3.0);
    complex c2(2.0);
    return 0;
}

(2)前置声明 & 类的声明

C/C++编译器是由上到下编译的,所以应该在函数/变量的使用前声明或定义,否则编译器找不到会报错。前置声明可以就是告诉编译器有哪些变量或函数,可以先使用,后面再进行定义。

//error: "use of undeclared identifier 'n'"
#include
using namespace std;
int func()
{
    return n += 5;
}
int n = 5;
int main()
{
    cout << func();
    return 0;
}

 

//output : 10
#include
using namespace std;
int func();
int n = 5;
int main()
{
    cout << func();
    return 0;
}
int func()
{
    return n += 5;
}

前置声明:解决两个类的互相依赖(类的声明和定义分成两个文件)。

参考:C++中前置声明的应用与陷阱

使用前置声明的注意:必须用指针或引用(前置声明的类还未定义,未定义构造函数);不能在A的声明中使用B的方法(B方法未详细定义);不能在A的声明中调用B的析构函数(未定义)

A.h
class B; //不用引入类的头文件,进行类的声明即可
class A{
private:
    B* b;
public:
    A();
    virtual ~A();
};

A.cpp
#include "A.h"
#include "B.h" //具体定义,需使用B的定义,引入B的头文件
A::A(){
    b= new B;
}

A::~A(){

}

B.h
class A; //不用引入类的头文件,进行类的声明即可
class B
{
private:
    A *a;

public:
    B();
    virtual ~B();
};
B.cpp
#include "B.h"
#include "A.h" //具体定义,需使用B的定义,引入A的头文件
B::B()
{
    a = new A;
}

B::~B()
{

}

前置声明还可以解决友元类的方法定义中。B的成员函数定义声明为A的友元函数,A需要B的定义;B的该成员函数使用A作为如参,B需要A的定义,循环依赖,使用前置声明。

// House.h
#include "Bed.h" //使用函数,需具体定义,引用头文件
class CHouse
{
    friend void CBed::Sleep(CHouse &);

public:
    CHouse(void);
    virtual ~CHouse(void);
    void GoToBed();
    void RemoveBed()
    {
    }
};

// House.cpp
#include "House.h" //.h文件中已经引入了Bed.h,不用重复引入

CHouse::CHouse(void)
{
}

CHouse::~CHouse(void)
{
    int i = 1;
}

void CHouse::GoToBed()
{
}

// Bed.h
class CHouse; //前置声明类,不用引头文件
class CBed
{
    int *num;

public:
    CBed(void);
    ~CBed(void);
    void Sleep(CHouse &);
};

// Bed.cpp
#include "House.h" //具体定义,引入House的头文件
CBed::CBed(void)
{
    num = new int(1);
}

CBed::~CBed(void)
{
    delete num;
}

void CBed::Sleep(CHouse &h)
{
}

类的声明:声明成员变量和一些函数(有些函数定义在类中,有些定义在类外)

(3)模版的简介:类的成员变量可以被定义为多种类型或成语函数可操作多种类型,因此可以将类定义为模版类,在具体定义对象时指定需传入的类型。

template
class complex
{
...
private:
    T re, im;
}

{
    complex c1(2.5,3.1);
    complex(int> c2(2,6);
}

(4)内联函数

函数在类內定义则称为内联函数,执行速度较快。但并非在类內定义的函数都是内联函数,只是对编译器的建议,复杂函数编译器无法内联。也可通过inline关键字修饰函数,表示内联,但也是对编译器的建议。

类的成员函数哪些可以称为内联函数:类内定义的函数;类内利用inline显式声明的函数;类内无inline显示声明,但类外利用inline显式定义的成员函数。在类内无inline显示声明,类外也无inline显示定义的成员函数,不是内联函数。

(5)访问级别

public:类內,类外均可访问

private:仅类内可访问。

(6)构造函数:创建对象时自动调用,不可直接调用,与类名相同,无返回值(在创建指定类型的对象,不用编写),可进行重载。

class comlpex
{
public:
    complex(double r=0, double i=0)
    :re(r), im(i) 
    {}
};

仅构造函数包含初值列,效率高。构造函数的初始化过程可以想象成:初值,赋值,初值列相当于在初值时的操作,效率高。

函数参数可以默认传参,在创建对象时,可传递参数,也可不传使用默认值。

{
    complex c1(2.0, 3.0);
    complex c2(5.0);
    complex* c3 = new complex();
}

以上对象的创建都调用上面带默认参数的构造函数。

重载:函数名相同,函数参数类型和个数不同。编译器通过函数名和参数生成唯一的函数标识。类似"func_name@double@double"(此处仅作为举例,实际存储不是这种样式)。以上带默认值的构造函数重载时,不可编写带零个double或一个double类型的参数,编译器查找函数时无法区分,会报错。

构造函数也可放在private区,外界不可创建对象,仅在累内创建,如单例模式。(了解即可)

(7)常量成员函数:当成员函数不改变调用的对象的数据时,将该成员函数定义为常量成员函数。

class complex
{
public:
    ...
    double real() const {return re;}
};

{
    const complex c1;
    c1.real();
}

定义为常量的对象仅能调用常量成员函数,否则报错。

(8)pass by value vs  pass by reference(to const)

class complex {
public:
    complex(double r=0,double i=0):re(r),im(i){}
    complex& operator +=(const complex&);
    double real() const {return re;}
    double imag() const {return im;}
private:
    double re,im;

    friend complex& __doapl(complex*, const complex&);
};

函数传递参数包含两种方式:按照值传递,无任何特殊符号,传递的参数值有多大,就将传递的参数整个压入函数的调用栈;按照引用传递(推荐,尽量使用,与指针类似,但更漂亮),利用&,在传递的参数类型后面添加,底层实现相当于传递指针,速度快(传递4字节),但可能会改变传入的参数,不希望改变,利用const修饰传入的参数。

ostream& operator << (ostream& os, const complex& x){
    return os << '(' << x.real() << ',' << x.imag() << ')';
}
{
    complex c1(2.0, 1.0);
    cout << c1;
}

函数一般作用在左侧的标志符上,所以上面的 "operator <<" 不能定义为complex类的成员函数,否则上面的函数调用应写为下面的方式。不能利用const修饰函数,会改变传入的参数os的值。推荐第一种写入方式。

ostream& complex::operator << (ostream & os)
{
    return os << '(' << this->real() << ',' << this->imag() << ')';
}
{
    complex c1;
    c1 << cout;
}

利用this指针调用函数,利用"->" 。

(9)return by value vs return by reference

return by reference :不可以返回局部(local)变量的引用。

传递者无需知道接收者是以reference形式接收。传递时,不用特意标注,方便代码的编写。

 inline complex& __doapl(complex* ths, const complex & r){
    ths->im += r.im;
    ths->re += r.re;
    return *ths;
}
complex& complex::operator += (const complex & r){
    return __doapl(this, r);
}

{
    complex c1(2,1);
    complex c2;
    c1 += c2;
}

(10)友元

class complex {
......
private:
    double re,im;

    friend complex& __doapl(complex*, const complex&);
};

inline complex &__doapl(complex* ths, const complex & r){
    ths->im += r.im;
    ths->re += r.re;
    return *ths;
}

complex类的数据成员re和im是私有的,正常情况下外部函数不可以对对象的这两个成员变量进行访问,但若在类内对外部函数进行friend(友元)声明,则该外部函数,可以访问该类对象的私有数据(自由取得friend的private成员)。

(11)相同class的各个objects互为friends(友元)

class complex {
public:
    complex(double r=0,double i=0):re(r),im(i){}
    double func(const complex& param){
        return param.re + param.im;
    }
private:
    double re,im;
};
{
    complex c1(2.0,1.0);
    complex c2;

    c2.func(c1);
}

(12)class外的各种定义:成员函数和全局函数的定义

class complex {
public:
    complex(double r=0,double i=0):re(r),im(i){}
    complex& operator +=(const complex&);
private:
    double re,im;

    friend complex& __doapl(complex*, const complex&);
};
inline complex &__doapl(complex* ths, const complex & r){
    ths->im += r.im;
    ths->re += r.re;
    return *ths;
}
complex& complex::operator += (const complex & r){
    return __doapl(this, r);
}

函数的成员函数,默认带有指向调用该函数的对象的指针this,在成员函数的传参里,无显式标注,但在成员函数的函数体中,可以直接使用。

(13)operator overloading

运算符重载可以使用两种方式实现,成员函数和非成员函数。只能选择一种实现。

complex& complex::operator += (const complex & r){
    return __doapl(this, r);
}

非成员函数的实现,不可以返回引用,两个对象想加,利用一个新的对象空间存储 ,该新的对象在函数结束后生命周期就结束了,不可以进行返回引用。

inline complex operator+(const complex& x, const complex& y){
    return complex(
        x.real()+y.real(),x.imag()+y.imag()
    );
}
inline complex operator+(const complex &x, double y)
{
    return complex(
        x.real() + y, x.imag()
    );
}
inline complex operator+(double x, const complex &y)
{
    return complex(
        x+ y.real(), y.imag()
    );
}
{
    complex c1(2,1);
    complex c2;
    
    complex c3=c1+c2;
    c3=c1+5;
    c3=2+c1
}

临时对象:typename()。生命在下一行就结束。

运算符重载 << ,注意输出从左向右进行,在连续输出时,应进行返回,若返回void,连续输出会报错(缺少一个参数)。

4. String class(带指针的类)

指针的好处可以根据创建的内容,动态的分配内存。

(1)Big Three (深拷贝)

创建String类型对象a,b

C++面向对象程序设计(侯捷)学习笔记(一)_第6张图片

拷贝构造:利用一个这个类存在的对象创建另一个对象,传入参数是自己这类对象

String(const String& str);

在类中无显示声明的拷贝构造函数(使用系统默认,将对象内容按位拷贝)。

C++面向对象程序设计(侯捷)学习笔记(一)_第7张图片

使a,b为同一指针的不同命名,修改内容会相互影响;直接拷贝赋值也会使得b指向的内容发生内存泄漏。

String::String(const String& str){
    mystr = new char[strlen(str.mystr) + 1];
    strcpy(mystr, str.mystr);
}

C++面向对象程序设计(侯捷)学习笔记(一)_第8张图片

拷贝赋值:将这个类一个对象的值赋值给这个类另一个对象。运算符重载= ,传入参数是自己这类对象

String& operator = (const String& str);

使用系统默认的拷贝赋值函数会出现与上面使用系统默认拷贝构造函数相同的情况。

inline String& String::operator=(const String& str){
    if (this == &str) {
        return *this;
    }
    delete[] mystr;
    mystr = new char[strlen(str.mystr) + 1];
    strcpy(mystr, str.mystr);
    return *this;
}

拷贝赋值分为3步:将原对象的内容清空,为原对象重新分配空间,将目的对象的数据内容拷贝给原对象。

C++面向对象程序设计(侯捷)学习笔记(一)_第9张图片

在进行上面三步前应包括一个自我检测,否则会出现下面的情况。

C++面向对象程序设计(侯捷)学习笔记(一)_第10张图片

在进行第一步删除数据后

C++面向对象程序设计(侯捷)学习笔记(一)_第11张图片

当想要访问str的mystr时,会出现不确定行为(undefined behavior)

析构函数:销毁对象自动调用,释放空间

~String();
inline String::~String(){
    delete[] mystr;
}

5. 堆&栈

(1)栈(Stack):存在于某作用域scope的一块内存空间。例如调用函数,函数本身就会形成一个栈,用于放置它接收的参数,局部变量以及返回地址。(作用域形成一个栈)

{
    int a=0;
}

(2)堆: 操作系统提供的一块全局的内存空间,程序可动态分配从中获得若干区块blocks。

class Complex{......};
{
    Complex c1(1,2);
    Complex* p = new Complex(2,3);
}

上面的例子中,c1所占用的内存空间来自于栈,指针变量 p的内存空间来自于栈,Complex(2,3) 是一个临时对象,其所占用的空间是由new从堆动态分配所得,并️指针p指向。

(3)stack object的生命周期

calss Complex{......};

{
    Complex c1(1,2);
}

c1是一个stack object,生命在作用域结束之际结束,系统自动调用析构函数。这种作用域内的object,又称为auto object,因为会被自动清理。

(4)static local objects生命周期

calss Complex{......};

{
    static Complex c2(1,2);
}

 c2是一个静态变量,其生命在作用域结束之后仍然存在,直到整个程序结束。

(5)global objects 生命周期

calss Complex{......};

Complex c3(3,4);

{
    static Complex c2(1,2);
}

c3是一个全局对象,生命在整个程序结束之后才结束,可以看做是一个特殊的static object。

(6)heap object生命周期

class Complex{......};
{
    Complex* p = new Complex(2,3);
}

指针p所指的其所占用的内存空间是由new在heap上动态分配所得的临时对象Complex(2,3)就是heap object,其生命在它被deleted之际结束。

上面的例子出现内存泄漏的情况,作用域结束,指针p的生命结束,但p所指的heap object对象仍存在,作用域外看不到p,也就没机会delete p,p所指的heap object对象所占用空间没机会释放。

(7)new&delete原理

new:先分配内存,再调用构造函数。

Complex* p = new Complex(1,2);

上面的代码编译器会转化为:

Complex* p 

1. void* mem = operator new(sizeof(Complex)); //分配内存,operator new内部调用malloc
2. p = static_cast(mem); //转型
3. p->Complex::Complex(1,2); //调用构造函数

 delete:先调用析构函数,再释放memory

delete p;

1. p->Complex::~Complex(); //析构函数
2. operator delete(pc); //释放内存,free(pc),释放pc所指heap object所占用内存

 

重点总结:

1. 仅在类中定义的函数或显示标识inline关键字的函数为内联函数。

2. 注意构造函数初值列的使用,提高效率。 

3. 类的数据成员一定标识为私有的。

4. 参数传递尽量使用引用传递。

5. 函数不会修改任何传入参数的值,可修饰为常量函数。函数有显式传入的参数,不会改变某个传入的参数的值,该传入的参数用const修饰。

 

 

你可能感兴趣的:(C++学习之旅,c++)