C++基础学习一 —— 关键字与运算符

文章目录

  • 关键字与运算符
    • 一、 指针与引用
    • 二、 define, typedef 和 inline
      • define 和 typedef 的区别
        • define:
        • typedef:
      • define 和 inline 的区别
        • 1. define
          • 无参宏定义
          • 有参宏定义
        • 2. inline 内联函数
          • inline的特性
          • C++ 中inline的编译限制
    • 三、override 和 overload
      • 1. override 是重写(覆盖)了一个方法
      • 2. overload 是重载
    • 四、new 和 malloc
      • 区别:
    • 五、constexpr 和 const
      • constexpr 变量
        • 必须使用常量初始化
      • constexpr 函数
      • constexpr 构造函数
      • const
        • 指针常量和常量指针
        • const 和 static 的区别
      • constexpr 的好处:
    • 六、volatile
    • 七、extern
    • 八、static
    • 九、前置 ++ 与 后置 ++
    • 十、 std::atomic

关键字与运算符

一、 指针与引用

  1. 指针存放某个对象的地址,其本身就是变量(命了名的对象),本身就有地址,所以可以有指向指针的指针;
  2. 引用就是变量的别名,从一而终,不可改变,必须初始化;
  3. 不存在指向空值的引用,存在指向空值的指针

二、 define, typedef 和 inline

define 和 typedef 的区别

define:
  1. 知识简单的字符串替换,没有类型检查
  2. 是在编译的预处理阶段起作用
  3. 可以用来放置头文件重复引用
  4. 不分配内存,给出的是立即数,有多少次使用就进行多少次替换
typedef:
  1. 有对应的数据类型,是要进行判断的;
  2. 是在编译、运行的时候起作用的;
  3. 在静态存储区中分配空间,在程序运行过程中内存中只有一个拷贝

define 和 inline 的区别

1. define

定义预编译时处理的宏,只是简单的字符串替换,无类型检查,不安全

无参宏定义

define定义的一般形式:

#define  标识符  字符串

其中,

#表示这是一条预处理命令。凡是以#开头的均为预处理命令

define 为宏定义命令,

标识符为所定义的宏名,

字符串可以是常数、表达式、格式串等。

例如:

#define MAXNUM 99999
有参宏定义

C++ 语言允许有参宏定义,在宏定义中的参数称为形参,在宏调用中的参数称为实参

对带参数的宏,在调用中,不仅要宏展开,而且要用实参去代换形参。

带参宏定义一般形式为:

#define  宏名(形参表)  字符串

注意: 在字符串中含有各个形参,在使用时调用带参宏调用的一般形式为:宏名(实参表)

例如:

#define add(x, y) (x + y)
int main()
{
    cout << "1 plus 1 is " << add(1, 1.5) << ".\n";
    //输出“1 plus 1 is 2.5.”
    system("pause");
    return(0);
}

这个“函数”定义了加法,但是其没有类型检查,有点类似于模板的感觉,但没有模板安全,可以看作是一个简单的模板。

注意:在其定义的内容(x + y)加了括号,这里加括号的原因是:宏定义只是在预处理阶段作了简单的替换,如果单纯的替换为 a + b a + b a+b时,当你使用 5 ∗ a d d ( 2 , 3 ) 5 * add(2, 3) 5add(2,3)时,其运算过程为 5 ∗ 2 + 3 5 * 2 + 3 52+3,而非我们想要的 5 ∗ ( 2 + 3 ) 5 * (2 + 3) 5(2+3)

2. inline 内联函数
inline的特性

inline是先将内联函数编译完成,生成了函数体,直接插入被调用的地方,减少了压栈、跳转和返回操作。没有普通函数调用时的额外开销;

内联函数是一种特殊的函数,会进行类型检查;

对编译器的一种请求,编译器有可能会拒绝这种请求;

C++ 中inline的编译限制
  1. 不能存在任何形式的循环语句
  2. 不能存在过多的条件判断语句
  3. 函数体不能过于庞大
  4. 内联函数声明必须在调用之前

三、override 和 overload

1. override 是重写(覆盖)了一个方法

以实现不同的功能,一般是用于子类在继承父类时,重写父类方法

规则:

    1. 重写方法的形参列表,返回值,所抛出的异常与被重写方法一致
      2. 被冲洗的方法不能为 `private`
      3. 静态方法不能被重写为非静态方法
      4. 重写方法的访问修饰符,一定要大于被重写方法的访问修饰符(**public > protected > default > private**)

2. overload 是重载

这些方法的名称相同而参数形式不同

一个方法有不同的版本,存在于一个类中

规则:

1. 不能通过访问权限、返回类型、抛出的的异常进行重载
 2. 不同的参数类型可以是不同的参数类型,不同的参数个数,不同的参数顺序(参数类型必须不一样)
 3. 方法的异常类型和数目不会对重载造成影响

使用多态,是为了避免在父类里大量重载,引起代码的臃肿且难于维护

重写和重载的本质区别是:加入了override 的修饰符的方法。 此方法始终只有一个被你使用的的方法

四、new 和 malloc

区别:

  1. new 内存分配失败时,会爆出 bac_alloc 异常,它不会返回NULL; malloc 分配内存失败时,返回NULL。

  2. 使用new 操作符申请内存分配时,无需指定内存块的大小,而 malloc 则需要显式地指出所需内存的尺寸。

  3. operator new/operator delete 可以被重载,而malloc/free 不允许重载。

  4. new/delete 会调用对象的构造函数/析构函数,而malloc/free则不会调用。

  5. malloc 与 free 是C++/C 语言标准库函数, new/delete 是C++ 运算符。

  6. new 操作符从自由存储区上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。

new/delete malloc/free
本质属性 运算符 CRT函数
内存分配大小 自动计算 手工计算
类型安全 是 (一个int 类型指针指向float会报错) 不是 (malloc类型转换为int, 分配double数据类型大小的内存空间不会报错)
两者关系 new 封装了 malloc
其他特点 除了分配和释放内存还会调用 构造和析构函数 只分配和释放内存
内存分配失败会抛出bac_alloc异常 内存分配失败会返回NULL
返回定义时具体类型的指针 返回的是void类型的指针,使用时需要进行类型转换

五、constexpr 和 const

const 表示“只读”的语义,constexpr 表示“常量”的含义

constexpr 只能定义编译期常量,而const 既可以定义编译期常量,也可以定义运行期常量。

constexpr 变量

复杂系统中很难分辨一个初始值是不是常量表达式,可以将变量声明为constexpr类型,由编译器来验证变量的值是否是一个常量表达式。

必须使用常量初始化
constexpr int n = 20;
constexpr int m = n + 1;
static constexpr int MOD = 10000007;

如果constexpr 声明中定义了一个指针,constexpr仅对指针有效,和所指向的对象无关。

constexpr int *p = nullptr;  // 常量指针 顶层 const
const int *q = nullptr; // 指向常量的指针,底层const
int *const q = nullptr; // 顶层const

constexpr 函数

constexpr 函数是指 能用于常量表达式的函数。

函数的返回类型和所有形参类型都是字面值类型,函数体有且只有一条return语句。

constexpr int new() {return 42;}

为了可以在编译过程中展开,constexpr函数被隐式转换为了内联函数。

constexpr 和内联函数 可以在程序中多次定义,一般定义在头文件

constexpr 构造函数

构造函数不能说const,但字面值常量类的构造函数 可以是 constexpr。

constexpr 构造函数必须有一个空的函数体,即所有成员变量的初始化都放到初始化列表中。对象调用的成员函数必须使用constexpr修饰。

const

指针常量和常量指针

指针常量: const int *d = new int(2);

常量指针:int *const e = new int(2)

区别方法:

左定值,右定向:指的是const*的左边还是右边

拓展:

顶层const:指针本身是常量;

底层const:指针所指的对象是常量;

若要修改const 修饰的变量的值,需要加上关键字 volatile

若要修改 const 成员函数中某些与类状态无关的数据成员,可以使用mutable关键字来修饰这个数据成员;

const 和 static 的区别
关键字 修饰常量【非类中】 修饰成员变量 修饰成员函数
const 1. 超出其作用域后空间会被释放; 2.在定义时必须初始化,之后无法更改; 3. const 形参可以接受const 和 非const类型的实参; 只在某个对象的生命周期内是常量,对于整个对象而言是可变的; 不能赋值,不能在类外定义; 只能通过构造函数的参数 初始化列表中初始化 【原因: 因为不同的对象对其const数据成员的值可以不同,所以不能在类中声明时初始化】 防止成员函数修改对象的内容【不能修改成员变量的值,但是可以访问】 const对象不可以调用非const 的函数; 但非const 的对象可以调用const 对象。
static 在函数执行后不会释放其内存空间 只能用在类定义体 内部的声明,外部初始化,且不加static ① 作为类作用域的全局函数【不能访问非静态数据成员和调用非静态成员函数】 ② 没有this指针 【不能直接存取 非类的非静态成员,调用非静态成员函数】 ③ 不能声明为virtual

**const 和 static 不能同时修饰成员函数。 原因:静态成员函数不包含有this指针,即不能实例化;而const成员函数必须具体到某一实例

constexpr 的好处:

  1. 为一些不能修改的数据提供保障,写成变量就有被意外修改的风险。
  2. 有些场景,编译器可以在编译期间对constexpr 的代码进行优化,提高效率。
  3. 相比宏来说,没有额外的开销,但更安全可靠。

六、volatile

定义:

  1. 与 const 绝对对立的,是类型修饰符影响编译器编译的结果, 用该关键字声明的变量表示该变量随时可能发生变化,与该变量有关的运算,不要进行编译优化;
  2. 会从内存中重新转载内容,而不是直接从寄存器中拷贝内容。

作用:

指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值,确保对特殊地址的稳定访问

使用场合:

在中断服务程序和cpu相关寄存器的定义

举例说明:

空循环:

for (volatile int i = 0; i < 10000; i++); // 一定会被执行,不会被优化掉

七、extern

定义:声明外部变量 【在函数或者文件外部定义的全局变量】

八、static

作用:实现多个对象之间的数据共享 + 隐藏,并且使用静态成员还不会破坏隐藏原则;默认初始化为0

九、前置 ++ 与 后置 ++

self &operator++() {
    node = (linktype)((node).next);
    return *this;
}
const self operator++(int) {
    self tmp = *this;
    ++*this;
    return tmp;
}

为了区分前后置,重载函数是以参数类型来区分,在调用的时候,编译器默默给int指定一个0

1. 为什么后置返回对象,而不是引用

因为后置为了返回旧值创建了一个临时对象,在函数结束的时候这个对象就会被销毁,如果返回引用,该引用毫无意义

2. 为什么后置前面也要加 const

其实也可以不加,但是为了防止你使用i+++++++++,连续两次调用后置++ 重载符,为什么呢?

原因:

它与内置类型行为不一致;

我们无法获得我们所期望的结果,因为第一次返回值是旧值,而不是原对象,如果连续调用两次 ++ ,结果只累加了一次,所以我们必须手动禁止其合法性,就要在前面加上const。

3. 处理用户的自定义类型

最好使用前置 ++ ,因为他不会创建临时对象,进而不会带来构造和析构而造成的额外开销。

十、 std::atomic

问题: a++int a = b 在C++中是否是线程安全的?

答案:不是

例1:

a++: 从C/C++ 语法的级别来看,这是一条语句,应该是原子的; 但是从编译器的角度来看,其实不是原子的。

其一般对应三条指令,首先将变量a对应的内存值搬运到某个寄存器(如eax)中,然后将该寄存器的值自增1,再将该寄存器的值搬运回到a代表的内存中。

mov eax, dword ptr [a] # (1)
inc eax # (2)
mov dword ptr [a], eax # (3)

现在假设 i = 0 i = 0 i=0,有两个线程,每个线程对变量a的值都递增1, 预想一下结果应该为2,但是实际运行结果为1,神奇

分析如下:

int a = 0;
// 线程1(执⾏过程对应上⽂汇编指令(1)(2)(3))
void thread_func1() {
a++;
}
// 线程2(执⾏过程对应上⽂汇编指令(4)(5)(6))
void thread_func2() {
a++;
}

我们预想结果是,线程1 和 线程2 的三条指令,各自执行,最终a的值变为2,但是由于操作系统线程调度的不确定性,线程1 执行完指令(1) (2) 后,eax的寄存器中 i 的值变为1,此时操作系统切换到 进程2 执行,执行(3) (4) (5),此时eax的值变为1; 接着操作系统继续执行,执行指令(6), 得到a的最终结果为 1 。

例2:

int a = b;:从C/C++ 的语法级别看,应该是原子的;但是从编译器的角度来看,由于现在计算机CPU体系的限制,数据不能直接从内存某处搬运到内存的另外一处,必须借助寄存器中转,因此这一条语句一般对应两条计算机指令,即:将变量b的值搬运到寄存器; 将寄存器值搬运到变量a的地址。

mov eax, dword ptr [b]
mov dword prt [a], eax

既然是两条指令,那么多个线程在执行这两条指令时,某个线程可能会在第一条指令执行完毕后被剥夺CPU时间片,切换到另一个线程而出现不确定的情况。

解决办法: C++ 11 新标准发布后改变了这种困境,新标准提出了对整形变量原子操作的相关库,即:std::atomic,这是一个模板类型:

template
struct atomic:

我们可以传入具体的整型类型对模板进行实例化,实际上STL库也提供了这些实例化的模板类型

// 初始化1
std::atomic value;
value = 99;
 
// 初始化2
// 下面代码在Linux平台上无法编译通过(即 gcc编译器)
std::atomic value = 99;
// 出错的原因在于 这行代码调用的是 std::atomic 的拷贝构造函数
// 而根据C++ 11 语言规范,std::atomic 的拷贝构造函数,使用=delete标记禁止编译器自动生成
// g++在这条规则上 遵循了C++ 11语言规范

你可能感兴趣的:(#,C++基础,c++,学习,开发语言)