C++学习笔记-从C到C++

C++支持的编程范式

  1. 面向过程编程:数据抽象 + C // 更好的C,不过看网上一些博文,好像C也可以进行面向对象式的编程。
  2. 面向对象编程:封装 + 继承 + 多态 // 为了实现多态,要增加额外的运行时信息,本来直接拿着地址去调用函数就行了,现在要额外的查一次表,所以速度应该会受到影响。
  3. 基于对象编程:function + bind // 实际上就是接口类提供挂载点,具体的功能类按需把具体的成员函数绑定到对应的挂载点, 应该是属于静态多态,因为具体调用的函数编译器就能确定。
  4. 泛型编程:将数据类型或者算法参数化,可以认为函数把固定类型的不同数据参数化了,一套处理逻辑(函数体)可以适用于不同的数据;而模板则把数据类型(当然也支持算法的参数化)也参数化了,这就大大减少了重复代码。我觉得可以把模板看成是函数重载和宏替换的综合体。
  5. 函数式编程:更多的使用递归而不是循环。

命名空间

参见我的C++学习笔记-命名空间,或者touzani的这篇C++ 命名空间namespace。

const常量

  • 定义常量:const int MAX_VAL = 23;

  • 定义指向常量的指针:

int a = 10;
const int *pa = &a; // <=> int const *pa = &a
                    // 不能通过pa修改a,但可以通过其它方式修改a,如直接修改  
  • 定义指针常量:
int a = 10;
int * const pa = &a; // pa是个指针变量,这个指针变量本身不可变,但可以通过pa修改a
  • 定义指向常量的指针常量:
const int a = 10;
const int * const pa = &a; // pa以及他指向的内存都不可改变
  • 修饰类的成员函数:
    表示该成员函数不会修改类的数据成员,该类的const对象只能访问const成员函数,而且const成员函数和该函数的非const版本构成重载。

  • 修饰函数参数,防止函数内部修改该参数;同样的,修饰函数返回值,一般用于返回引用,防止被修改。

  • const修饰的变量可以用于switch的分支条件(只接受常量表达式),C中是不行的。

const可以代替#define的宏常量(C中不敢叫常量,因为其实可以被修改,C++中大胆地这么叫了),但是inline函数不可以可以完全代替#define的宏函数,因为当参数是常量时,宏函数可以作为switch的分支条件语句,但是inline函数不行。

#define SUM(x,y) ((x)+(y))

inline int Sum(int x, int y)
{
    return x + y;
}

const int i = 10;
int aa = 10;
switch (aa) {
    case i:               // ok
        break;
    case SUM(10,20):      // ok
        break;
//  case Sum(20,30):      // failed
//      break;
    default:
        break;
}

因此C++11中新增了一个constexpr叫做常量表达式,于是我们可以这样:

#define SUM(x,y) ((x)+(y))

constexpr int Sum(int x, int y)
{
    return x + y;
}

const int i = 10;
int aa = 10;
switch (aa) {
    case i:               // ok
        break;
    case SUM(10,20):      // ok
        break;
    case Sum(20,30):      // OK
        break;
    default:
        break;
}

constexpr强制被他修饰的函数或者表达式编译期求值,前提是函数参数或者参与表达式的因子在编译期可以确定值,而不是等到运行时采取调用函数求值。

const强调的是‘不变’,constexpr强调的是‘编译期求值’。

数组

C++中定义数组时,其大小可以由变量指定,C中是不行的:

int max = 0;
cin >> max;
int array[max] = {
    0};
cout << "sizeof(array): " << sizeof(array) << endl;
// IN: 20
// OUT: sizeof(inline_array): 80

引用

引用不是变量,只是变量的别名,它没有自己的内存空间,它与所引用变量共享内存空间。

引用必须初始化,引用一经初始化,不能再指向其它变量。

const与引用

  • const引用:所引用的对象不能被修改。

  • 非const引用不能指向const对象。

  • const引用可以指向非const对象,只是不能使用这个引用修改所引用的对象的值。

  • const引用可以使用类型转换特性,非const引用不支持使用类型转换,只能指向同类型的对象。

float fval = 10.0f;
const int &ref1 = fval; // ok
// int &ref2 = fval; // failed
  • 非const引用不能绑定临时对象,临时对象只能用const引用绑定。

引用作为函数参数

C语言中函数参数传递是值传递,如果有大块数据作为参数传递的时候,采用的方案往往是指针,因为这样可以避免将整块数据全部压栈,可以提高程序的效率。C++中又增加了一种同样有效率的选择,就是引用。

引用作为参数传递,使得形参与实参在内存中共享存储单元,函数内对形参的任何操作直接作用于实参,与传递指针的效果相同。

而使用指针作为函数的参数虽然也能达到与使用引用的效果,但是在被调函数中同样要给形参分配存储单元,且需要重复使用”*指针变量名”的 形式进行运算,这很容易产生错误且程序的阅读性较差;另一方面,在主调函数的调用点处,必须用变量的地址作为实参。

所以能用引用就尽量不要用指针。

指针与引用

  1. 指针是一个实体,而引用仅是个别名;
  2. 引用使用时无需解引用(*),指针需要解引用;
  3. 引用只能在定义时被初始化一次,之后不可变;指针可变;
  4. 引用不能为空,指针可以为空

引用作为函数返回值

好处是函数返回引用不会产生临时对象。如果返回值,那函数在返回的时候,会调用返回对象的拷贝构造函数构造一个临时对象(得是个类,普通变量就是一个简单的赋值),然后返回这个临时对象,这无疑会拖慢程序运行速度,而且会产生不必要的内存开销。当然如果使用gcc的默认选项,不一定会看到拷贝构造函数的调用,因为gcc默认会进行返回值优化(RVO),要想观察该过程,可以给gcc添加参数:-fno-elide-constructors

但是需要注意

  • 不能返回局部变量的引用。

  • 不能返回函数内部new分配的内存的引用。例如,被函数返回的引用只是作为一 个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就无法释放,造成memory leak。

函数重载

函数重载是指的参数不同的同名函数可以同时存在,但如果仅仅是返回值不同,参数完全相同,则无法进行重载。前面说过,参数相同的类成员函数的非const与const版本也构成重载。

需要注意的一点是,当函数形参有默认值时,如下面代码中的func函数,当给两个实参时,func(int a, int b)与func(int a, int b, int c = 5)都能匹配上,就会出现二义,编译器不知道用哪个。

void func(int a, int b)
{
    cout << "a + b: " << a + b << endl;
}

void func(int a, int b, int c = 5)
{
    cout << "a + b + c: " << a + b + c << endl;
}

int main(void)
{
    // func(3, 5);  // 出现二义性
    func(3, 4, 5);  // 没问题

    return 0;
}

运算符重载

运算符也是函数,只是调用形式比较特殊而已。

其实一个程序无非就是“数据+动作”,这个数据就是变量、常量,即使是文件或者数据库,也得转化为某种变量来操作,而动作其实就是函数。面向对象,只是把这两个东西封装了起来,而泛型编程则是把函数参数类型也作为参数了。另外,如果偏重于逻辑的话,数据也可以叫做数据结构,函数也可以叫做算法。

那既然函数可以重载了,运算符自然也可以:

struct Complex
{
    float real;
    float imag;
};

Complex operator+ (const Complex &left, const Complex &right)
{
    Complex complex;
    complex.real = left.real + right.real;
    complex.imag = left.imag + right.imag;

    return complex;
}

std::ostream& operator<< (std::ostream &cout, const Complex &complex)
{
    cout << complex.real << "+i" << complex.imag;
    return cout;
}

int main(void)
{
    Complex complex1, complex2;
    complex1.real = 1;
    complex1.imag = 1;
    complex1.real = 2;
    complex1.imag = 3;
    cout << complex1 + complex2 << endl;

    return 0;
}

因为还没学习到类那里,所以这里我就完全以面向过程的形式来写的代码,其实这种形式与类的友元函数形式的运算符重载是一样的。

如果是双目运算符,那就传两个参数,在第一个参数传运算符左边的操作数,第二个参数,传右边的操作数;单目运算符,传一个就行了。

后面会看到,以类成员函数的形式重载运算符,可以少写做操作数这个参数,但不是所有操作符都适合重载成成员函数,像上面代码的第二个输出流插入运算符,就不适合。

名称改写(name managling)

C语言是不支持函数重载的,C++之所以支持函数重载,是因C++编译器在编译C++的时候,有个名称改写的机制:

int abs(int val)
{
    return val > 0 ? val : -val;
}

float abs(float val)
{
    return val > 0 ? val : -val;
}

int main(void)
{
    abs(-5);
    abs(-0.3f);

    return 0;
}

经过预处理和编译之后,得到的汇编文件:

    .file   "test.cpp"
    .text
    .globl  _Z3Absi
    .type   _Z3Absi, @function
_Z3Absi: #-------------------------- 这里对应第一个函数
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    %edi, -4(%rbp)
    movl    -4(%rbp), %eax
    cltd
    movl    %edx, %eax
    xorl    -4(%rbp), %eax
    subl    %edx, %eax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   _Z3Absi, .-_Z3Absi
    .globl  _Z3Absf
    .type   _Z3Absf, @function
_Z3Absf: #-------------------------- 这里对应第二个函数
.LFB1:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movss   %xmm0, -4(%rbp)
    movss   -4(%rbp), %xmm0
    pxor    %xmm1, %xmm1
    ucomiss %xmm1, %xmm0
    jbe .L9
    movss   -4(%rbp), %xmm0
    jmp .L7
.L9:
    movss   -4(%rbp), %xmm1
    movss   .LC1(%rip), %xmm0
    xorps   %xmm1, %xmm0
.L7:
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE1:
    .size   _Z3Absf, .-_Z3Absf
    .globl  main
    .type   main, @function
main: #----------------------------- 这里对应main函数
.LFB2:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    $-5, %edi
    call    _Z3Absi  #-------------- 调用int版本(第一个)的函数
    movss   .LC2(%rip), %xmm0
    call    _Z3Absf  #-------------- 调用float版本(第二个)的函数
    movl    $0, %eax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE2:
    .size   main, .-main
    .section    .rodata
    .align 16
.LC1:
    .long   2147483648
    .long   0
    .long   0
    .long   0
    .align 4
.LC2:
    .long   3197737370
    .ident  "GCC: (GNU) 7.1.1 20170528"
    .section    .note.GNU-stack,"",@progbits

从汇编代码中很容易看出来,对于编译器“GCC: (GNU) 7.1.1 20170528”来说,函数int abs(int val)经过名称改变后函数名变为_Z3Absi,其中i代表的就是有一个int类型的参数,相应的float abs(float val)的函数名改写为_Z3Absf,不同的编译器甚至同一编译器的不同版本,名称改写方式都有可能不同,这是导致C++语言二进制兼容性差的一个很主要的原因。

extern “C”

extern “C” {}表示{}中的代码按照C编译器规则进行编译,不进行名称改写;不使用名称改写,就不能使用函数重载功能。

这个用于C代码,并不是用于C++代码,主要是编写C语言库的时候,希望C++语言也可以调用该库,则这些库代码在头文件中的声明必须用extern “C”包裹起来,防止C++编译器编译的时候对里面的名称进行改写导致与动态库进行连接的时候出错。

语法:

#ifdef __cplusplus
extern "C"
{
#endif
void func(double a)
{

}

void func2(int a)
{

}
#ifdef __cplusplus
}
#endif

内联函数

所谓内联就是编译器会在编译时将函数调用用函数代码来替换,类似与宏函数。

什么时候用:代码体短小精悍,经常调用的函数适合声明为内联函数,函数前面加inline。

需要注意的是,inline关键字只是给编译器的一个建议,具体会不会内联,要看具体的代码,比如包含循环的话,编译器不会内联。

内联函数与带参数的宏区别:

  • 内联函数调用时会进行实参和形参的类型检查,宏函数只是简单的把形参替换为实参。

  • 内联函数是在编译的时候将代码展开,而宏函数是在预处理的时候将代码展开。

C++类型转换

旧式(C风格):(T)expr、T(expr)

新式(C++风格):

  • static_cast(expr):
    C++语言认可的类型转换,如char到int,double到float,对于具有转换构造函数或者类型转换运算符的类也可以使用。

  • reinterpret_cast(expr):
    C++语言不认可的类型转换,如int*到int;将数据以二进制存在形式重新解释,如指针类型;不会做任何对齐操作,C风格的会。

  • const_cast(expr):
    移除对象的常量属性,主要用于指针或者引用。去除指针或引用的const限定的目的不是用来修改它的内容,而是为了函数能够接受这个实参

void func(int &val)
{
    cout << "func " << val << endl;
}

int main(void)
{
    const int val = 100;
    // 函数形参是非const的,实参是const的,不能直接用传递,需要去const.
    func(const_cast<int&>(val)); 

    return 0;
}
  • dynamic_cast(expr):
    用于动态转换,安全的向下、向上转型,用于派生类与基类之间的转型操作,支持运行时识别。

new/delete

new/delete对应于C语言的malloc/free,但new/delete除了分配堆内存(operator new、operator delete)还会做额外的事:

使用new表达式时发生的三个步骤:

  • 调用名为operator new的标准库函数,分配足够大的原始的未类型化的内存,以保存指定类型的一个对象;

  • 运行该类型的一个构造函数去初始化对象;

  • 返回指向新分配并构造的构造函数对象的指针。

使用delete表达式时,发生的两个步骤:

  • 调用对象的析构函数;

  • 调用名为operator delete的标准库函数释放该对象所用的内存。

operator new 和operator delete函数有两个重载版本:

void * operator new (size_t);
void * operator new[](size_t);

void operator delete(void *);
void operator delete[](void *);

你可能感兴趣的:(c++,c++)