为什要学习C++

  • C++具有多种优良的特性

    • C++是由C语言扩展而来,继承了C语言高效的语法,比如可以使用指针,可以直接访问内存。
    • C++具有高级语言的特性,同时C++是编译型语言,执行效率高。
    • 利用C++进行编程相对于使用C语言来说更加快捷,增加代码复用性。
  • 所以C++是那些有C语言基础的人员进阶理解高级语言特性最为合适的语言。

C++语言的难点

C++支持多种编程范式,语法极其丰富且紧随潮流,支持下面这些编程方式:

  • 面向过程编程
  • 面向对象编程
  • 函数式编程
  • 泛型编程

我们需要掌握C++中的重点内容

  • 堆的使用、函数重载、默认参数、引用
  • 类的定义与使用(权限、成员、构造析构)
  • 继承(权限、多继承)

我们需要掌握C++语言的重要特性

  • 类型转换
  • 静态、友元运算符重载
  • C++对象的内存模型
  • 以长处理

define、const、enum、inline总结

define

define是使用非常广泛的预处理命令

  • 无参宏

    • 定义一个常量的值,便于修改
    • 定义一组属性,增强可读性
  • 有参宏

    • 用于经常被调用的短小代码,提高程序的运行效率

尽管define有以上优点,但是他还有一些致命的缺点

  • 纯替换,有参宏中容易产生误解
  • 没有类型检查,调用中容易出错
  • 没有指出这些属性值是有关联的

示例:

#define MAX(a,b) a>b?a:b

int main()
{
    int n1 = 1,n2 = 0;
    int n3 = MAX(n1,n2);
    //在有参宏中使用自增运算符的时候,就导致了逻辑上的错误
}

const

  • const是一个神奇的关键字,神奇之处在于告诉编译器这个变量不允许改变,编译器保证其不会改变。(他仍然还是一个常量,但是它再也不能被赋值了)
  • const定义的常量是由类型的,在编译的时候会进行类型安全检查,这是const优于define的地方,所以我们推荐使用const代替define定义常量。
  • const处理定义变量之外还有很多的用法:

    • 函数参数声明为const
    • 返回值声明为const
    • 类成员函数声明为const

const是一个常量修饰符,在C++中常量的一些使用规则:

  1. 常量必须被初始化
  2. 在使用常量的时候,编译器会直接将常量名用它的初始值来进行替换(和无参宏一样,在源码中使用宏的名字,经过预处理之后会被替换成宏的内容)

const使用场合

修饰指针
常量指针:将指针指向的内容修饰为常量.(指针可以被指向被const修饰过的变量,这就意味着我们不能通过指针来修改引用的值 )
示例:

char szBuff[100] = {"hello"};    //这个会直接存到栈区
const char* pStr = "Hello";            //这个存在常量区
szBuff[0] = 'A';//可以修改
//p就是常量指针
//1.指向的内容被修饰为常量,因为不能通过p去修改内存。
//2.p自身是可以被修改的
//3.const只是在语法层面上限制一个指针不能修改它指向的内容
//,至于指针指向的内容能不能被修改是不受const的改变。
const char* p;    //可以不初始化
p = szBuff;
const char * p1 = "hahaha";
szBuff[0]  = 'B';//仍然可以被修改
p[0] = 'A';//语法错误,p不可以被修改,不能通过p修改
p1[0] = 'A';//语法错误
//只要一开始的时候,数据不是在常量数据去都可以被修改,常量数据区不可被修改(存的是字符串)

常量指针总结:
指针可以修改为指向不同的常量指针
可以修改为指向不同的变量
可以通过解引用来读取指针指向的数据
不可以通过解引用修改指针指向的数据

指针常量:将指针自身修饰为常量
示例:

char szBuff[100] = {"hello"}
//定义一个指针常量p,指针常量必须被初始化
char* const p = szBuff;
//1.指针不能再指向其他内存
p = "hello";//错误,p只能指向szBuff
p[0] = 'A';//在语法上是通过的
printf("%s",szBuff);//这个最后输出的内容是Aello
//char * const p2 = "hahaha";
//p2[0] = 'A';
//在语法上是通过的,在运行的时候就报错,因为p2 存的是一个常量字符串的地址,常量字符串不能修改            

指针自身不可以被修改,指针指向的值可以被修改

定义指针的时候,*在const左边,指针是一个常量指针
定义指针的时候,*在const右边,指针是一个指针常量

修饰引用

int n = 100;
// 定义一个引用.
int& rNum = n;
rNum = 10; // 实际就是在修改变量n
// 常量引用. 
// 不能rNum2去修改了.
const int& rNum2 = n;
rNum2 = 20; // 报语法错误

修饰成员函数
实际修饰的是this指针,也就是将this指针修饰成常量指针。

Class MyClass
{
    int m_nNum;
public:
    void fun()
    {
        m_nNum = 100;
    }
}

void fun(const MyClass* pObj)
{
    pObj->fun();
    //语法错误
    
    //原因分析:
    //1. pObj是一个常量指针
    //2. 成员函数有条件修改自身的成员变量,这相当于通过指针调用了成员函数,成员函数在内部修改了成员,实际上就相当于间接通过指针修改了指针指向的内存,这个是常量指针所不允许的。
}

修改版:

class MyClass
{
    int m_nNum;
public:
    //const修饰的是this指针
    //this就相当于const MyClass* this;
    void fun() const
    {
        this->m_nNum = 100;//此处会报语法错误
        //报错:错误1.表达式必须是可修改的左值,错误2.由于正在通过常量对象访问m_nNum,因此挖法对其进行修改
    }
}

void fun(const MyClass* pObj)
{
    pObj->fun();//此处不会报错
}

总结:
在成员函数后加上const , 成员函数就是一个常量成员函数, 在这样的函数内部不能修改自身的成员变量, 也不能调用其它的非常量成员函数。
通过常量对象指针(const MyClass* pObj), 只能调用常量成员函数.

引用

性质

  • 引用类型不占用内存空间,其内存空间来自被引用的变量

    • 定义的时候必须初始化
    • 不能再引用其他变量
    • 修改自身的时候,被引用的变量被会被修改,被应用的变量修改了,引用类型的变量也会被修改,因为他们使用的是同一块内存空间。

使用场合

传参

void swap(int& l,int& r)
{
    int t = l;
    l = r;
    r = t;
}

int main()
{
    int n1 = 10,n2 = 20;
    swap(n1,n2);//执行之后,n1n2交换
}            

传递函数返回值

int& fun(int& n)
{
    return n;
}

int main()
{
    int nNum = 100;
    fun(nNum) = 200;    //修改了nNum
    int n2 = fun(nNum);
    n2 = 10;    //nNum没有被修改,因为n2不是引用类型的变量,它拥有自己的内存空间,所以修改了n2不会影响nNum
    int & rNum = fun(nNum);
    rNum = 10;    //修改nNum的内存空间,因为rNum是一个引用
}

类型转换
const_cast:常量类型转换-去除常量类型。

void fun(const char* pStr)
{
    pStr[0] = 'A';    //语法报错
    //1.C语言风格
    char * p = (char*)pStr;
    p[0] = 'A';        //不会报错
    //2.C++风格
    char *p2 = const_cast<char*>(pStr);//类似于(char*)pStr
}

int main()
{
    char buff[100];
    fun(buff);
}

static_cast:静态类型转换-编译器认可的,例如char转为int

void fun(int n)
{
}

int main()
{
    //编译器3.14从double类型隐式转换成int 类型再传参
    //C语言风格
    fun(3.14);//语法可以通过,但是会报一个警告:精度丢失
    
    fun((int)3.14);//强制转换为和形参一样的类型
    //2. C++
    
    fun(static_cast<int>(3.14))
}

reinterpret_cast:强制类型转换-编译器不认可的,例如int*int

        用于编译器无法进行隐式转换时的类型转换
int main()
{
    int * p = 0x300000;    //报错:语法错误,类型不匹配
    
    //C语言风格
    p = (int*)0x300000;    //将我们赋的值强转为和p一样的类型
    //C++风格
    p = reinterpret_cast<int*>(0x300000);
}

dynamic_cast:动态类型转换,用于将父类和子类的指针或应用进行转换(继承的时候会用到,能够将其类转换为派生类)

class Base
{
};
class MyClass:public Base
{
    int nNum[100];
};
class B:public Base
{
    int m_nNum;
};

int main()
{
    Base* obj = new B;
    MyClass* pObj = obj;    //语法错误
    //Base* 类型的值不能用于初始化MyClass* 类型的实体
    
    //可以在语法上通过。
    //但在运行的时候会出现严重问题,因为obj本质上
    //并非是MyClass的类型,而是B类型
    pObj = (MyClass*)obj;
    
    //dynamic_cast可以检测是否能够转换
    //如果不能就返回nullptr
    pObj = dynamic_cast<MyClass*>(obj);
}

enum

经常遇到这样的问题:为某一些属性定义一组可选的值,例如:

#define UP 0
#define DOWN 0
#define LEFT 0
#define RIGHT 0

缺点是从语法上,没有指出这些值是有关联的,enum就是它的一种替代方法,不但可以定义整数常量,还可以分组。

enum DIR
{
    UP,
    DOWN,
    LEFT,
    RIGHT
};

使用枚举在函数传参的时候还可以进行参数检查。

枚举常量代表该枚举类型的变量可能取的值,编译系统为每个枚举常量指定一个整数值,默认状态下,这个整数就是所列举元素的序号,序号从0开始。 可以在定义枚举类型时为部分或全部枚举常量指定整数值,在指定值之前的枚举常量仍按默认方式取值,而指定值之后的枚举常量按依次加1的原则取值。 各枚举常量的值可以重复

inline

inline能够实现和有参宏一样的替换功能,不同的是inline会检查参数而有参宏不会检查参数类型。

规则:

  • inline关键字必须和函数体定义放在一起才可以实现内联,仅仅将inline放在函数声明之前不起任何作用。
  • 在函数名前加上inline关键字这个函数才可能称为内敛函数。
  • inline指令就像register,他只是对编译器的一种提示,而不是命令。也就是说,只要编译器愿意,他就可以随意地忽略掉你的指令,事实上编译器也会这么做。例如,大多数编译器拒绝内敛“复杂”的函数(例如循环和递归的函数)

称为内敛函数的标准:

  • 函数体必须小(代码行数少)
  • 函数内部不能有复杂的逻辑(switch,递归函数、各种嵌套的循环或选择结构都是不行的)
  • 用法和函数一样
  • 功能和有参宏差不多(直接将函数调用替换为函数体的代码)
inline int max(int a,int b)
{
    return a>b?a:b;
}

int main()
{
    int n1 = 1,n2 = 2;
    //正常方式调用函数
    //但是表意之后,这里的函数会替换成上面的函数体
    //就类似于有参宏在预编译之后会被替换一样
    //但是如果实参是表达式的时候,会先计算出表达式的结果,然后再进行替换
}

内联函数你调用一万次,他就重新定义一万次,也算是内联的一个缺点,但是内联一般较小,影响不大。

总结:
通过上面的介绍,在C++中,建议尽量少的使用#define。
1.使用const与enum代替无参宏的使用
2.使用inline代替有参宏的使用

最后修改:2020 年 08 月 26 日
如果觉得我的文章对你有用,请随意赞赏