堆空间的申请和释放

C中通过malloc、free,在堆空间中开辟、释放内存。
C++中通过new、delete,在对空间中开辟、释放内存。

#include <iostream>
#include <stdlib.h>
using namespace std;
int main()
{
    int *pBuf = new int[15];
    int *pNum = new int(15);
    wcout << *pNum << endl;
    delete pNum;
    delete[] pBuf;
    pNum = nullptr;
    pBuf = nullptr;
    system("pause");
    return 0;
}

重载与名称粉碎

重载
相同作用域,如果两个函数名称相同,但是参数不同,我们称它们为重载或者overload
函数重载又称为函数的多样性

  • 形参数量不同
  • 形参类型不同
  • 形参的顺序不同
  • 形参数量和形参类型都不同

名称粉碎

  • C++为了支持重载的这种特性,进而提出了名称粉碎/名字改编(name managling)机制,构成重载的同名函数可能在编译器中分别为:

    • -?Add@@YAHHH@Z
    • -?Add@@YAHHH@Z
  • 也就是说重载的函数进行了名字改编,编译器看到的重载的函数名字其实是不同的。
  • 如果在函数前面加了extern "C" 就代表不进行函数改编,所以C语言是不支持函数重载。

默认参数

在C++中,函数声明的时候,可以给形参赋一些默认值,调用函数的时候,若没有给出实参,则按指定的默认值进行工作。
例如:

int Add(int nNumA, int nNumB, int nNumC = 2);    //默认参数需要写在最后
int Add(int nNumA = 1, char cChar = 2, int nNumC = 3);

//当有默认参数的时候,如果有该函数的声明则只需在声明中写默认参数即可
//参考接下来的规则
int Add(int nNumA, int nNumB, int nNumC)
{
    printf("我是第一个\n");
    return nNumA + nNumB + nNumC;
}
int Add(int nNumA, char cChar, int nNumC)
{
    printf("我是第二个\n");
    return nNumA + cChar + nNumC;
}

int main()
{
    Add();        //执行第二个
    Add(1, 2);    //执行第一个
    Add(1);        //执行第二个
    Add(1, 2, 3);//执行第一个
}

在C++中,函数声明或则和定义的时候,可以给形参赋一些默认值,调用函数时,若没有给出实参,则按指定的默认值进行工作。

规则:

  • 函数没有声明的时候,在函数定义中指定形参的默认值
  • 函数既有定义又有声明的时候,声明指定后,定义就不能再指定默认值
  • 默认值的定义必须遵守从左到右的顺序,如果某个形参没有默认值,则它左边的参数就不能有(默认参数必须在所有没有默认参数的右边)

注意:
使用默认参数同时还使用了重载容易造成二义性。

面向对象简介

  • 编写程序是为了解决生活中的实际问题,在计算机中,使用一定数据来表示问题,按照一定的逻辑来处理这些数据,所以得出
    程序 = 算法 + 数据结构
  • 在早期,程序如同魔法师一样使用机器语言编写程序,更多的是要站在机器的角度考虑问题从解决问题的方法到实现为机器语言程序,思维跳跃很大。
  • 后来有了汇编语言,但汇编语言仅仅时集器指令的助记符,还是要站在机器角度
  • 直到出现了高级语言,屏蔽掉了计算机内部的执行细节,使得我们可以更多心思放在解决问题上。
  • 传统的程序设计方法称之为结构化设计,这种方法自顶向下设计,将系统视为处理多个问题子程序的相互配合,这种设计方式让程序更加有条理性。
  • 结构化程序设计为处理复杂问题提供了有力手段,但到80年代末,这设计方法逐渐暴露一下缺陷:

    • 程序难以管理
    • 数据修改存在问题
    • 程序可重用性差
  • 结构化程序设计在考虑问题时仅仅以算法为中心,忽略了算法与数据之间的内在联系,造成了数据与数据处理的分离。
  • 面向对象陈伟一颗耀眼的明星,他是一种以对象为中心的思维方式。
    对象 = 算法 + 数据结构
    程序 = 对象 + 对象 + 对象
  • 每个对象自己用自己的方法管理数据,暴露在外面的是对象提供的接口,数据是封装的。
  • 随着时间的发展,面向对象的思想慢慢发展出了三大基本特征

    • 封装
    • 继承
    • 多态

类与对象

C++语言中,类与对象是实现面向对象思想的载体。
类就是自定义的数据类型,包含了数据与方法,对象就是类定义出来的变量。
类的定义包括两个部分:

  • 声明部分

    • 数据成员的声明
    • 成员函数的声明
  • 实现部分

    • 成员函数的实现
//声明部分
class <类名>
{
    public:    <共有段数据及成员函数>
    protected: <保护段数据及成员函数>
    private: <私有段数据及成员函数>
}
//实现部分
<各成员函数的实现>

成员变量

成员变量是一个对象的数据组成,一个对象的内存空间就是由成员变量组成。
成员在类中的声明顺序决定了成员变量在内存中的顺序。

成员变量的使用
首先我么需要提供一个对象,才能使用成员变量
在成员函数内部,this指针来表示当前对象

class MyClass
{
    int m_nNum1;
    int m_nNum2;
public:
    void fun() {
        // this也可以不写, 
        this->m_nNum1 = 10;
        // 不写的时候编译器会自动加上.
        m_nNum2 = 100;
    }
}

在类的外部,只能通过对象来访问成员变量

int main()
{
    MyClass obj;
    //必须通过对象来使用成员变量
    obj.m_nNum2 = 10;
}

成员函数

定义成员函数

定义成员函数可以采用以下三种方式:

  • 成员函数的定义以及实现在类体内完成
  • 成员函数的定义以及实现在类体外完成
  • 成员函数的定义以及实现类体在不同文件中完成

成员函数可以重载以及设置默认参数,规则与普通函数一致。

成员函数内部会自带一个this指针,这个指针是从哪里来的?

  • 通过对象调用函数的时候,C++会自动将对象的内存首地址赋值给成员函数内部的this指针,这样一来,成员函数被调用之后this指针就保存着调用了这个成员函数的对象变量。

普通成员函数

  • 需要通过对象才能调用
  • 通过this指针也能调用
  • 其他情况和普通函数一样,也可以设置默认参数,也可以进行函数重载

构造函数

构造函数作用于创建类对象时,初始化其成员

  • 对象被创建时,自动被调用
  • 构造函数于类同名,没有返回值
  • 构造函数可以有参数,故而构造函数可以构成重载,并且可以有默认参数

构造函数是一个特殊的成员函数,它的作用是用于构造一个对象。它没有返回值,因为返回值默认是一个对象。
构造函数只能被自动调用。

  • 我们定义变量时(定义局部变量,全局变量)
MyClass g_obj;    //调用构造函数
int main()
{
    MyClass obj1;    //调用构造函数
}
  • 从堆空间申请对象时
int main()
{
    MyClass* pObj = new MyClass;    //调用构造函数
}
  • 函数形参
void fun(MyClass obj){}

int main()
{
    MyClass obj1;    //调用了构造函数
    fun(obj1);        //会为形参obj调用构造函数,因为进入函数的时候,需要为形参obj开辟内存空间
    fun(obj1);        //会为形参obj调用构造函数
}
  • 函数返回值
class MyClass
{
public:
    MyClass()
    {
        printf("构造函数");
    }

};

MyClass fun()
{
    MyClass obj;
    return obj;
}

int main()
{
    MyClass obj2;            //这里调用一次构造函数
    MyClass obj3 = obj2;    //这里不调用构造函数
    MyClass obj1 = fun();    //这里只会调用一次构造函数,是在fun函数中创建对象时调用的
}

综合示例

class CDesk
{
public:
    int m_high,m_width,m_length,m_weight;
};
class CStool
{
public:
    CStool()
    {
        m_high = 1;
        m_width = 2;
        m_length = 3;
        m_weight = 4;
    }
    int m_high,m_width,m_length,m_weight;
}
CDesk g_objDesk;    //全局对象
CStool g_objStool;    //全局对象

int main()
{
    CDesk objDesk;    //局部对象
    CStool objStool;    //局部对象
    CDesk *pobjDesk = new CDesk;    //堆对象
    CStool *pobjStool = new CStoll;//堆对象
    CStool arrStool[50];    //局部对象数组
    CDesk arrDesk[50];        //局部对象数组
    delete pobjDesk;        
    delete pobjStool;
    return 0;
}

拷贝构造

C++中,提供了用一个对象值创建并初始化另一个对象的方法,完成该功能的是拷贝构造函数

//拷贝构造函数的格式如下:
<类名>::<构造函数>(<类名> &<引用名>)
{
    <函数体>
}
CLocation::CLocation(CLocation &obj)
{
    
}

如果一个类中没有定义拷贝构造函数,则系统会自动生一个成默认拷贝构造函数,其功能只是简单的把形参的对应属性赋值给新对象。

拷贝构造的特点:

  • 拷贝构造函数只有一个参数,为该类类型的引用

被调用的时机:

  • 用于使用已知对象的值,创建一个同类的新对象
  • 系统在两个位置常常调用拷贝构造:

    • 把对象做为实参进行函数调用时,系统自动调用拷贝构造函数实现把对象值传递给形参对象。
    • 当函数返回值为对象时,系统自动调用拷贝构造函数创建一个临时对象作为返回值

什么情况使用拷贝构造函数:
类的对象需要拷贝时,拷贝构造函数将会被调用。以下情况都会调用拷贝构造函数:

  • 一个对象以值传递的方式传入函数体
  • 一个对象以值传递的方式从函数返回
  • 一个对象需要通过另外一个对象进行初始化。

带参的构造函数以及初始化列表

构造函数
不带参数的构造函数不能完全满足初始化的要求,因为这样创建的类对象具有相同的初始值。
如果需要对类对象按不同的特征初始化出不同的值,应采用带参数的构造函数。

初始化列表
const成员的初始化只能在构造函数初始化列表中进行
引用成员的初始化只能在构造函数初始化列表中进行
成员属性为类类型,且其对应的类没有无参构造函数,它的初始化也只能在构造函数初始化列表中进行

例如:

class CLocation
{
public:
    CLocation(int nNumA, int nNumB)
    {
        m_X = nNumA;
        m_Y = nNumB;
    }
    int getx()
    {
        return m_X;
    }
    int gety()
    {
        return m_Y;
    }
private:
    int m_X, m_Y;
};


class CLocation1
{
public:
    CLocation1(int nNumA, int nNumB) :m_X(nNumA), m_Y(nNumB)
    {
    }
    int getx()
    {
        return m_X;
    }
    int gety()
    {
        return m_Y;
    }
private:
    int m_X, m_Y;
};

int main()
{
    CLocation obj1(1, 2);
    CLocation1 obj1(1, 2);
}

转换构造

  • 将一个标准类型变量赋值给另一个标准类型的变量时,如果这两种类型兼容,则会做隐式转换后赋值。
  • 当把一个变量赋值给对象时会发生同样的事。系统尝试把这个变量转换为类对象。
  • 怎么把变量转换为类对象

    • 以这个变量作为参数,去尝试匹配构造函数。如果匹配上了,则通过此构造函数创建临时变量。这个构造函数实际上时间接做了转换的活,所以这个构造函数又可以叫做转换构造。
  • 转换构造并不特指某个固定的构造函数,只要能完成转换的功能,都可以叫做转换构造
  • 特点时只有一个参数(不绝对)
class CNumber
{
public:
    CNumber(int nNum):m_nNum(nNum){}//用explicit限制转换
private:
    int m_nNum;
};

int main()
{
    CNumber objA(12),objB = 34;
    objA = 56;    //这里调用转后构造函数将34和56转换为CNumber类型
    return 0;
}

构造函数可以进行重载

当构造函数没有进行重载的时候,只能调用无参的构造函数,无参构造函数时C++编译器默认提供的一个构造函数(不需要定义就有),但是如果定义了其他版本的构造函数,编译器就不再踢狗无参构造。

在进行构造函数重载之后,构造对象的时候,就可以通过传参来决定调用哪个重载版本

class MyClass
{
public:
    //重载了构造函数
    MyClass(){}
    MyClass(int n){}
    MyClass(double d){}
    MyClass(int n,double d){}
    MyClass(MyClass& obj){}
}
void fun(MyClass obj){}

MyClass fun2()
{
    return 2;
}

int main()
{
    //在这里会为obj1调用转换构造函数
    //相当于MyClass obj1(2);
    MyClass obj1 = fun2();
    
    //转换问题看示例后面的隐式转换
    fun(5);//不会报错,且会将形参obj调用构造函数:MyClass(int n){}
    
    //在构造对象的时候,归根据实参来调用不同版本的构造函数
    MyClass obj1(5);    //MyClass(int n){}
    fun(obj1);            //给形参obj调用了构造函数,选择的构造函数是:MyClass(MyClass& obj){}
    
    MyClass *p = new MyClass(5,1);    //MyClass(int n,double d){}
    MyClass obj2(obj1);    //MyClass(MyClass& obj){}
}

隐式转换
C++编译器会尝试将整型的实参5(上面的例子中)转换成MyClass类型的形参,但C++编译器是不能没有任何依据地转换。
规则:形参是类类型,并且有一个构造函数刚好可以将实参传递进去。
此时编译器就可以将实参传递给形参的构造函数,直接构造出形参,而不是直接将实参赋值给形参。

构造函数的一些术语:

  • 默认构造:指的是没有形参的构造函数,由编译器默认提供,在某些场合编译器需要自动调用一个类对象的构造函数的时候,只能调用默认构造。例如:子类继承了父类,当子类对象被构造的时候,父类的构造也会被自动调用,此时就只能调用父类的默认构造。
  • 转换构造:值得是那些只有一个形参,且数据类型是非本类类型的构造函数们。一般能够显式调用(例如:MyClass obj(5)),也能够隐式调用:fun(5);fun的形参是MyClass类型。
  • 拷贝构造:值得是只有一个形参,且参数类型是本类类型的应用。一般是在定义一个对象的时候,将另一个对象作为初始值的时候,就会自动调用这个版本的构造函数,一般编译器会默认提供一个拷贝构造,默认提供的拷贝构造会将对象的内存空间进行拷贝。
  • 带参构造:含有两个以上的形参的构造函数统称带参构造。

析构函数

析构函数用于撤销类对象,释放其资源

  • 对象被销毁时,自动被调用
  • 析构函数名为~类名,没有返回值
  • 无参数,只有一种固定的写法,不能重载

析构函数和构造函数相反,当一个对象被销毁的时候,就会调用析构函数

示例:

class CTest
{
    CTest()
    {
        m_szName = new char[20];
    }
    ~CTest()
    {
        delete[] m_szName;
    }
protected;
    char *m_szName;
}

什么时候应该在析构函数中添加内容?

  • 占用了资源(打开了文件),在析构中统一释放(关闭文件)
  • 为成员变量开辟了堆空间,可以在析构函数中统一释放

构造与析构的顺序

class CMonitor
{
public:
    CMonitor()
    {
        cout << "构造 显示器\n";
    }
    ~CMonitor()
    {
        cout << "析构 显示器\n";
    }
};

class CKeyboard
{
public:
    CKeyboard()
    {
        cout << "构造 键盘\n";
    }
    ~CKeyboard()
    {
        cout << "析构 键盘\n";
    }
};

class CComputer
{
public:
    CComputer()
    {
        cout << "构造 电脑\n";
    }
    ~CComputer()
    {
        cout << "析构 电脑\n";
    }
protected:
    CMonitor m_objMonitor;
    CKeyboard m_objKeyboard;
};

int main()
{
    CComputer * com = new CComputer();    //显式调用无参构造
    delete com;
    return 0;
}

最终结果:
首先构造显示器,接着键盘,最后电脑
首先析构电脑,接着键盘,最后显示器

隐式转换

总结下类构造函数隐式转换的必要条件:

  • 找不到传参类型严格对应的函数
  • 找到传参类型严格匹配的类的构造函数
  • 因为隐式转换构造出的是临时对象,所以不可修改,故触发隐式转换的函数的传参类型必须要使用const修饰

构造析构总结

构造函数是一种用于创建对象的特殊成员函数,调用它为类对象分配空间,给他的数据成员赋初值,以及其他请求资源的工作。

析构函数是一种用于撤销对象,回收对象占有资源的特殊成员函数,它与构造函数的功能互补,成对出现。

每个类对象必须在构造函数中诞生,一个类可以有一个或多个构造函数,编译程序按对象构造函数声明中使用的形参数与创建对象的实参数进行比较,确定使用哪个构造函数,这与普通重载函数的使用方法相似。

在包含有对象成员的类对象被创建的时候,需要对对象成员进行创建,相应的要调用对象成员的构造函数

拷贝构造函数用于由一个一致的对象创建一个新的对象

运算符重载函数
稍后仔细介绍

访问控制

  1. public : 类内类外都能访问
  2. protected : 控制在子类内部能访问, 在类外不能访问
  3. private : 在子类和类外都不能访问

对象

对象的定义

定义完对象类型之后,就可以通过类类型声明变量了,类类型的变量也称之为对象。
类名 对象名;
CLocation objLocation

和普通变量一样,类对象的定义有多种方式
CLocation objA,objB,objC[10],*pojbD;

+ objA、objB为两个一般对象
+ objC[10]是一个对象数组
+ pobjD是指向类CLocation对象的指针

对象的使用

对象访问成员的方法与结构体变量访问成员的方法相同

  • 访问一般对象的成员:

    • 对象名.数据成员名
    • 对象名.成员函数名(<参数表>)
  • 访问指向对象的指针的成员:

    • 对象指针名->数据成员名
    • 对象指针名->成员函数名(<参数表>)

.->的名字叫分量运算符

C++中的类与结构体
跟标准中的结构体不同,C++中的结构体,增加了定义函数的特性。使得C++中结构体与类几乎没有区别。

  • 类定义中默认情况下的成员访问级别是private
  • 结构体定义默认情况下的成员访问级别是public

this指针

成员函数必须是以哦那个对象来调用。一个类所有对象调用的成员函数都是同一代码段,例如:

void CTdate::set(int m,int d,int y)
{
    m_Month = m;
    m_Day = d;
    m_Year = y;
}

成员函数如何识别m_Month、m_Day和m_Year是属于哪个调用对象呢?
实际上,在每个类的成员函数中,都隐含了一个this指针,该指针指向正在调用成员函数的对象。

this指针是C++实现封装的一种机制,它将对象和该对象调用的成员函数连接在一起,在外部看来,每一个对象都拥有自己的函数成员。一般情况下,并不写this,而是让系统进行默认设置。
this指针永远指向当前对象。

例:
当对象s调用s.set(2,10,1999)的时候,在我们看来他是传递了3个参数吗,但是实际上传递了4个参数,其中还包括一个隐含的this指针,这个指针就是这个对象的地址。

void CTdate::set(int m,int d,int y)
{
    m_Month = m;
    m_Day = d;
    m_Year = y;
}
s.set(2,10,1999);
//实际上是
void CTdate::set(CTdate * this,int m,int d,int y)
{
    this -> m_Month = m;
    this -> m_Day = d;
    this -> m_Year = y;
}
s.set(&s,2,10,1999);

作用域

类的作用域简称类域,类域的范围是指在类所定义的类体中,该类的成员局限于该类所属的类域。一个类的任何成员都能访问同一类的任一其它成员。

在类作用域外对一个类的数据成员或成员函数的访问受程序员编写程序的控制。把成员定义为私有保护的时候,就会限制外部对其进行访问。

C++中新增加了作用域标识符::,有以下作用

  • 在小作用域内对外部同名标识符的访问
  • 用于表示类的成员
  • 用于命名空间中变量与函数的访问

作用域的可见性
类名允许与其他变量名或函数名同名,可以通过下面方法实现正确的访问。

  • 如果类型名隐藏了,可以通过加前缀class访问
class Sample{}
void func(int Sample)    //这种情况下形参将类名屏蔽
{                    //不能仅仅使用Sample来创建对象
    Sample++;    //形参自增
    class Sample a;    //如果要访问类名需要加一个class 
}
  • 如果类型名隐藏了变量,则通过走用于运算符::访问一般对象的成员
int Sample = 0;

class Sample
{
    void func()
    {
        printf("%d",::Sample)    //::表示全局作用域
    }
    
}

嵌套类

class Outer
{
public:
    //如果这个类是共有的,相当于这两个类是平行关系,如果是私有的,相当于隐藏了这个类
    class Inner
    {
    public:
        void Fun();
    };
public:
    Inner obj_;
    void Fun()
    {
        cout << "Outer::Fun" << endl;
        obj_.Fun();
    }
};

void Outer::Inner::Fun()
{
    cout << "Inner::Fun" << endl;
}

int main()
{
    Outer obj11;
    obj11.Fun();
    Outer::Inner obj22;
    obj22.Fun();
    return 0;
}

局部类

类可以定义在函数体内,这样的类被称为局部类。局部类只在定义它的局部域内可见。
局部类的成员函数必须被定义在类体中。
不可以在局部类中定义局部变量。

void Fun()
{
    class LocalClass
    {
    public:
        int m_num;
        void Init(int num)
        {
            m_num = num;
        }
        void Display()
        {
            cout << "num=" << m_num << endl;
        }
    };
    LocalClass lc;
    lc.Init(10);
    lc.Display();
}

int main()
{
    Fun();
    //LocalClass lc; //错误,局部类只能在定义它的函数体内使用
    return 0;
}
最后修改:2020 年 08 月 26 日
如果觉得我的文章对你有用,请随意赞赏