一、语法增强

1. 全局变量检测增强

c语言代码:

int a = 10; // 赋值,当作定义
int a; // 没有赋值,当作声明
int main() {
    return 0;
}

c

没有报错,可以执行。

image-20201211103203307

如果仅仅int a,内存空间存在。

image-20201211103123914

当单独int a,会定义a,赋初值0

当同时int a = 10; int a;,第一条语句是定义,第二条语句仅声明。

c是弱语法语言,没有问题。

C++中无法使用,重定义

2. 类型检测

void func(i) {
    i++;
}
// C中传参可以不写类型 C++中不可以,必须严格声明类型

3. 更严格的类型转换

C++中,不同类型的变量一般是不能直接赋值的,需要相应的转换。

二、 结构体增强

  1. C中定义结构体变量需要加上struct关键字,C++不用。

  2. C中结构体只能定义成员变量,不能定义成员函数。

    C++即可以定义成员变量,也能定义成员函数。

三、boolean 类型

bool true false

一个bool类型一个字节,true1, flase0

四、三目运算符的增强

  1. C语言的三目运算表达式返回值为数据值,为右值,不能赋值;
  2. C++语言三目运算表达式返回值为变量本身(引用),为左值,可以赋值

左值和右值的概念

C++中可以放在赋值操作符左边的是左值,可以放到赋值操作符右边的是右值。有些变量可以当左值,也可以当右值。

左值是 Lvalue , L 代表 Location ,表示内存可以寻址,可以赋值。

右值是 RvalueR 代表 Read , 就是可以知道它的值。

比如 int temp = 10; 中, temp 在内存中有地址, 10 没有,但是可以 Read到它的值。

五、const

C语言中的const

默认外部连接

// c 语言 的 `const' 修饰的全局变量 默认是外部连接的
// 外部连接: 其他源文件 可以使用
// func.c
const int num = 100; // 只读的全局变量

// main.c
extern const int num; // 声明 不赋值
printf("%d\n", num); // 输出 100

// 如果是全局变量,文字常量区, 只读
// 局部变量 栈区 内存可读可写
// 局部只读变量可以通过地址修改
// 但: 如果知道num的地址,可以通过地址间接地修改num的值
const int data = 100;
int *p = (int *)&data;
*p = 200;
printf("%d", data); // 200
  1. C 中的 const 修饰全局变量 默认是 外部连接的

    全局变量不可改

  2. 局部可读变量可以通过地址修改

总结

在C语言中:

  1. const 修饰全局变量 num 变量名只读,内存空间在文字常量区(只读)、不能通过 num 的地址修改空间内容;
  2. const 修饰局部变量 data 变量名只读,内存空间在栈区(可读可写),可以通过 data 地址 间接修改空间内容;

C++ 中的 const

默认内部连接

// func.cc
const int num = 100; // 默认内部连接

extern const int num = 100; // 设定为外部连接

赋值常量写入符号表

尝试修改值,而不会被修改:

    const int data = 100;

    int *p = (int *)&data;

    *p = 2000;
    cout << "data = " << data << endl; // data 的值还是 10
    cout << "p = " << *p << endl;

调试状态下,内存空间会默认被开辟:

image-20201211143932404

image-20201211144012903

此时, datap 指向同一片空间,而修改时,两者依旧相同:

修改值

值被修改了!

打印的时候却是100

打印的时候却没有修改。

const 声明的 data ,实际上并不在那片内存空间上!

C++ 中会对 const 的变量创建一个符号表,直接读取,而当要取出该变量的时候, C++编译器会开辟一片空间。而通过变量名访问的时候,还是从符号常量表中查到该值。

开辟空间的情况

  1. 申请常量的地址时;

  2. 用一个普通变量赋值到常量时:

    int b = 200;
    const int a = b;

    当完成赋值

    完成赋值,两者值相同,接下来更改内容:

    内容修改后

    由于 p a 指向的地址相同,修改 p 时就会修改 a 所属于的值。按照上一种情况,符号常量表中会存储 a, 访问 a 时会直接去找符号常量表的内容。但是:

    a被修改了

    我们可以看到, a 的值被改变了!

    很明显,这种常量定义的情况,符号表不会存储定义的变量,而是内存开辟一片存储内容,而这片内容可以通过地址间接修改。

    如果 b 也是常量呢?

        const int b = 100; // b 在符号表中
        const int a = b; // a 也会到符号表中 相当于 a 就是 b

    输出结果:

    符号表中有值

    如果将一个常量赋值给一个非常量呢?

    const int b = 100;
    int a = b;
    
    // 这个最终结果和 前面是一样的 
    // 也就是说
    int b = 100;
    const int a = b;
    
    // 两种情况下 对于 a 来说内存是可变的
    // 但注意 const 声明的变量是不能直接修改的
  3. 自定义数据类型,会分配内存 系统会分配空间(结构体、对象)

    符号表只会记录简单的数据类型

建议:

总结

  1. const int data = 10; // data 先放入符号表
  2. 如果对 data 取地址 系统才会给 data 开辟空间
  3. const int a = b; // b 是变量名 系统直接给 a 开辟空间 而不放入符号表
  4. const 修饰自定义数据 系统为自定义数据开辟空间

六、补充

1. 尽量使用const替换#define

C++ 尽量 使用 const 替换 #define

#define 仅仅替换, 很难查找到错误

const 有作用域

作用域

宏不能作为命名空间的成员 const 可以

七、引用(reference)

给已有变量取别名

1. 语法:

  1. &和变量名结合 表示 引用
  2. 给某个变量取别名 就定义某个变量
  3. 从上往下替换
int num = 10;
int &a = num; // 引用必须初始化
int *b = &num; // 取地址

// a 完全等价于 num

2. 注意:

  1. 引用必须初始化

  2. 引用一旦初始化,就不能再次修改别名

    int num = 10;
    int &a = num;
    
    int data = 20;
    a = data; // 不是 data 别名 为 a, 而是 将 data 值赋值 a(num)

3. 一些探讨

两者的内存

再尝试查找对应的地址:

    int data = 100;
    int &a = data;
    int &b = data;
    int &c = a;

a b c

此时,a b c 都指向 data 的地址 。

尝试加入一个 p 指针,对比与引用的区别:

引用和指针

可以看出,引用虽然在理解上是起别名,但在实际实现上就是存储地址,当使用到该别名的时候,就会通过该变量中指向的地址去找变量。

而指针是直接指向所需变量的地址,引用相当于在指针上加一层封装,最终的效果还是找到值存在的地址。

引用在底层上实现了它就类似一个变量,而实际上是一个指向所引用对象的地址。

引用并非指针,指针直达地址,更快,引用中存储地址,有一层封装。但相对来说,引用不是直接操作指针,地址不可修改,而指针可修改。相对来说,引用更安全。

但是程序取其地址的时候:

地址是一致的

三个变量指向的地址是一样的,这是一种设计。实际,引用变量实际地址在内存中并非是相同的地址,而是包装一个指向所引用变量的地址,当取地址时,会返回该地址。

所以,引用是占用内存空间的!但引用比直接使用指针安全。

给数组起别名

int arr[10] = {0};
int (&my_arr)[10] = arr;

// int &my_arr[10] 每个元素都是一个引用

typedef int TYPE_ARR[10];
TYPE_ARR &my_arr_2 = arr;

函数的引用

引用作为函数的参数

// 写一个交换函数 
void my_swap(int &a, int &b) {
    int tmp = a;
    a = b;
    b = tmp;
}
// 内部做的操作

引用作为函数的返回值

注意:

  • 函数返回值是引用时,不要返回局部变量
  • 当函数为左值时,那么函数值类型必须是引用
// 当函数为左值时,那么函数值类型必须是引用
int& my_data() {
    static int num = 10;
    cout << "num = " << num << endl;

    return num;
}

void test() {
    my_data() = 2000;
    my_data();
}
// 主要用于运算符重载。

引用的本质是常量指针

引用的本质在 C++ 内部实现是一个指针常量。

Type& ref = val ,实际上是 Type* const ref = &val

C++ 编译器在编译过程中使用常指针作为引用的内部实现,因此引用所占用的空间大小与指针相同,只是这个过程是编译器内部实现的,用户不可见。

int data = 10;
int &a = data;
// 编译器内存转换 int * const a = &data; // 不能改变
a = 100;

// 实际的处理过程交给编译器做

实际地址

在手动复制的过程中,两者的地址是一样的。但如果直接使用引用,调试状态下的引用变量的地址是不一样的。(疑惑?)

指针的引用

char* &mystr;

常引用

*引用没有开辟独立的空间??? sizeof 无法测试引用的 空间 *

一旦引用代表别名,则测试引用实际上是测所引用的变量。

常引用的格式:

const Type& ref = val;

注意: 字面量不能赋给引用,但是可以赋给常引用。const 修饰的引用,不能修改。

使用场景: 常引用主要用在函数的形参,尤其是类的拷贝或复制构造函数。

*将函数的形参定义为常引用的好处: * 引用不产生新的变量,减少形参与实参传递时的开销。由于引用可能导致实参随形参改变而改变,将其定义为常引用可以消除这种副作用。如果希望实参随着形参的改变而改变,那么使用一般的引用。

常量的引用:

// 10 是 const int 类型
const int &num = 10;