C/C++中的一些关键字理解

C++的语法相对其它语言而言,繁杂而琐碎,很多概念与用法容易混淆和遗忘,本文对一些基础概念进行总结,必要时及时补充,起到温习和提示的作用。

0.declaration 与definition

声明(declaration)用来告诉编译器变量的名称和类型,而不分配内存,也不赋初值。

定义(definition)为了给变量分配内存,可以为变量赋初值。

注:定义要为变量分配内存空间;而声明不需要为变量分配存储空间。

C++中语法实际上没有这两个关键字,这两个关键字只是语法结构的一种抽象。

1.初始化与赋值

在C++中,初始化与赋值是两个完全不同的操作。

初始化不是赋值,初始化的含义是创建变量时赋予一个初始值,而赋值的含义是把对象的当前值擦除,而用一个新值替代。

简单地说,初始化强调从无到有,对应变量的产生,赋值强调已有值被替换,对应变量值被修改。

1.1 列表初始化

使用花括号来进行列表初始化,当用于内置类型的变量时,这种初始化有一个特点就是,如果我们使用的列表初始化变量存在丢失值的风险,则编译器会报错,相当于提到提前检查的作用。

long double ld = 3.1415926536;  //声明并定义变量ld
int a{ld}, b = ld; //报错,转换未执行,因为存在丢失信息的风险,列表初始化给出错误提示
int c(ld), d = ld; //正确,转换执行, 非列表初始化, 丢失了部分信息,但不给出错误提示

1.2 默认初始化

如果定义变量时,没有指定初值,则变量被默认初始化,默认值到底是什么,由变量的类型决定。同时定义变量的位置也会对此有影响。

如果是内置类型的变量未被显式初始化,它的值由定义的位置决定。

定义于任何函数体之外的变量都被初始化为0.

定义于函数体之内的内置类型变量将不被初始化。

一个不被初始化的内置类型变量的值是未定义的。

任何试图拷贝或访问一个不被初始化的此类值将引发错误。

每个类各自决定其初始化对象的方式。是否允许不经初始化就定义对象也由类自己决定。

绝大多数类都支持无须显式初始化而定义对象,这样的类提供了一个合适的默认值。

std::string empty;  // empty非显式地初始化为一个空字符串
Sales_item item;    // 被默认初始化的Sales_item 对象

有一些类要求每个对象必须显式初始化,如果创建这样的对象而没有显式初始化将引发错误。

2.extern 

一个文件如果想要使用别处定义的名字,则必须包含对那个名字的声明。变量只能被定义一次,但是可以被声明多次,比如在多个文件中使用别处定义的同一个变量,那么多个文件都各声明一次。

如果想声明一个变量,而不是定义它,就要在变量的前面加上extern,而且不要显式地初始化变量

extern int i;  //声明变量i而非定义i
int j;  //声明并定义变量j,默认初始化,没有显式初始化

// 任何包含了显式初始化的声明就是定义。给extern关键字标记的变量赋初始值,那么就抵消了extern的作用


extern double pi = 3.141516;  // extern 语句赋初始值就不再是声明,而是定义

关键字 extern 可以应用于全局变量、函数或模板声明。 它指定符号具有 external 链接。有关链接的背景信息以及为何不鼓励使用全局变量,需参阅翻译单元和链接。

关键字 extern 具有四种含义,具体取决于上下文:

1) 在非 const 全局变量声明中,extern 指定变量或函数在另一个转换单元中定义。 必须在除定义变量的文件之外的所有文件中应用 extern。

2) 在 const 变量声明中,它指定变量具有 external 链接。 extern 必须应用于所有文件中的所有声明。 (默认情况下,全局 const 变量具有内部链接。)

3) extern "C" 指定函数在别处定义并使用 C 语言调用约定。 extern "C" 修饰符也可以应用于块中的多个函数声明。

4) 在模板声明中,extern 指定模板已在其他位置实例化。 extern 告知编译器它可以重复使用另一个实例化,而不是在当前位置创建新实例。

2.reference

引用 reference 包括左值引用和右值引用,一般笼统的说引用,就是指的左值引用。

左值引用是一个左值 lvalue (比如下面一个定义好的变量 i),定义左值引用带有一个 “&”

右值引用是一个右值 rvalue(比如临时量 i * 42),定义右值引用带有两个 “&&”

//左值引用举例
// 左值引用本身不是一个对象, 只是已经存在的对象的别名, 也不能定义引用的应用

int ival = 1024;
int &reVal = ival; // reVal 指向 ival, 是 ival的另外一个别名
int &reVal2  // 报错, 引用本身不是一个对象,必须初始化,绑定到某个变量
int &reVal3 = 3;  // 报错, 引用类型的初始值必须是一个绑定到的对象

//右值引用举例

int i = 42;
int &r = i; // r 引用 i, 是 i 的另外一个别名
int &&rr = i; // 错误, rr是一个右值引用,不能将一个右值引用绑定到一个左值上
int &r2 = i * 42; // 错误, i * 42是一个右值, r2是左值引用
const int &r3 = i * 42; // 可以将一个const引用绑定到一个右值上
int &&rr2 = i * 42; // rr是一个右值引用,绑定到乘法结果(右值)上

 3.const

 const类型的有常量指针(指针本身是一个常量)与指针常量(指针指向的是一个常量)

const double pi = 3.14;    // pi是个常量,它的值不能变
const double *cptr = π  // 就近原则, const 修饰指向的量是常量(双精度常量)

int errNumb = 0;
int * const curErr = &errNumb;  // 就近原则, const 修饰curErr, 指针是常量,一直指向errNumb 
const double *const pip = π  // pip 是一个指向常量对象的常量指针

顶层const 表示指针本身是一个常量,底层const表示指针所指的对象是一个常量,拷贝时,顶层const可以被忽略,因为拷贝后的对象可能不具有顶层const的属性,具体依拷贝后对象的类型而定,但是底层const属性一直存在,比如拷贝后不能通过拷贝对象改变原来具有底层const属性的对象值。

4.nullptr

nullptr 出现的目的是为了替代 NULL。在某种意义上来说,传统 C++ 会把 NULL、0视为同一种东西,这取决于编译器如何定义 NULL,有些编译器会将 NULL定义为 ((void*)0),有些则会直接将其定义为 0

C++ 不允许直接将 void * 隐式转换到其他类型。但如果编译器尝试把 NULL定义为 ((void*)0),那么在下面这句代码中:

char *ch = NULL;

没有了 void * 隐式转换的 C++ 只好将 NULL定义为 0。而这依然会产生新的问题,将 NULL定义成 0 将导致 C++ 中重载特性发生混乱。考虑下面这两个 foo 函数:

void foo(char*);
void foo(int);

那么 foo(NULL); 这个语句将会去调用 foo(int),从而导致代码违反直觉。

为了解决这个问题,C++11 引入了 nullptr 关键字,专门用来区分空指针、0。而 nullptr 的类型为 nullptr_t,能够隐式的转换为任何指针或成员指针的类型,也能和他们进行相等或者不等的比较。代码中 std::is_same 用于比较两个类型是否相同。

可以尝试使用 clang++ 编译下面的代码:

#include <iostream>
#include <type_traits>

void foo(char *);
void foo(int);

int main() {
    if (std::is_same<decltype(NULL), decltype(0)>::value)
        std::cout << "NULL == 0" << std::endl;
    if (std::is_same<decltype(NULL), decltype((void*)0)>::value)
        std::cout << "NULL == (void *)0" << std::endl;
    if (std::is_same<decltype(NULL), std::nullptr_t>::value)
        std::cout << "NULL == nullptr" << std::endl;

    foo(0);          // 调用 foo(int)
    // foo(NULL);    // 该行不能通过编译
    foo(nullptr);    // 调用 foo(char*)
    return 0;
}

void foo(char *) {
    std::cout << "foo(char*) is called" << std::endl;
}
void foo(int i) {
    std::cout << "foo(int) is called" << std::endl;
}

output:

foo(int) is called
foo(char*) is called

从输出中我们可以看出,NULL不同于 0 与 nullptr 。所以,请养成直接使用 nullptr 的习惯。

此外,在上面的代码中,我们使用了 decltype 和 std::is_same 这两个属于现代 C++ 的语法,简单来说,decltype 用于类型推导,而 std::is_same 用于比较两个类型是否相同。

5. constexpr

5.1常量表达式

常量表达式(const expression)表示在编译过程就能得到计算结果的表达式,用常量表达式初始化的const对象也是常量表达式。

一个对象或者表达式是不是常量表达式由它的数据类型和初始值共同决定。

const int max_files = 20;    // max_files 是常量表达式
const int limit = max_files + 1;  // limit 是常量表达式

int staff_size = 27; // staff_size 不是常量表达式
const int sz = get_size();  // sz不是常量表达式 

// 注意:staff_size的初始值是一个字面值常量,但是它的数据类型是int 非const int,所以它不是常量表达式
// sz是一个常量,但是它的值要直到运行时才能获取得到,所以也不是常量表达式

constexpr int mf = 20;     // 20是常量表达式
const int limit = mf + 1;  // mf + 1 是常量表达式
const int sz = size();     // 只有当size是一个constexpr 函数时,才是一条正确的声明语句

C++11新标准规定,允许将变量声明为constexpr类型以便由编译器来验证(检查)变量的值是否是一个常量表达式,不符合那么就会自然报错。

一般来说,如果你很确定变量是一个常量表达式,那么可以把它声明为constexpr类型。

指针和引用都能声明为constexpr类型,但是它的初始值却受到严格限制,一个constexpr指针的初始值必须为0或nullptr,或者是存储于某个固定地址中的对象。

函数体内定义的变量一般来说并非存放在固定地址中,因此constexpr指针不能指向这样的变量。

定义在函数体外的变量,其地址固定不变,能用来初始化constexpr指针。

允许函数定义一类有效范围超出函数本身的变量,比如static变量,这类变量和定义在函数体外的变量一样都有固定地址,constexpr引用能绑定到这样的变量,constexpr指针能指向这样的变量。

特别注意:在constexpr声明中如果定义了一个指针,限定符consexpr仅对指针有效,而对指针所指的对象无效,可以理解为 constexpr 是一个修饰符修饰的是对象本身,而不是对象的类型。

const int *p = nullptr;        // p是一个指向整型常量的指针
constexpr int *q= nullptr;     // q是一个指向整数的常量指针, constexpr 修饰的是 q 
//这里的关键字constexpr把定义的对象q设置为顶层const

constexpr int *np = nullptr;    // np是一个指向整数的常量指针, 其值为空

int j = 0;
constexpr int i = 42;  // i的类型是整型常量

// 下面i 和j 都必须定义在函数体之外

constexpr int *p = &i;  // p是一个常量指针, 指向整型常量 i
constexpr int *p2 = &j;  // p2是一个常量指针, 指向整数 j

5.2一些举例

C++ 本身已经具备了常量表达式的概念,比如 1+23*4 这种表达式总是会产生相同的结果并且没有任何副作用。如果编译器能够在编译时就把这些表达式直接优化并植入到程序运行时,将能增加程序的性能。一个非常明显的例子就是在数组的定义阶段:

#include <iostream>
#define LEN 10

int len_foo() {
    int i = 2;
    return i;
}
constexpr int len_foo_constexpr() {
    return 5;
}

constexpr int fibonacci(const int n) {
    return n == 1 || n == 2 ? 1 : fibonacci(n-1)+fibonacci(n-2);
}

int main() {
    char arr_1[10];                      // 合法
    char arr_2[LEN];                     // 合法

    int len = 10;
    // char arr_3[len];                  // 非法

    const int len_2 = len + 1;
    constexpr int len_2_constexpr = 1 + 2 + 3;
    // char arr_4[len_2];                // 非法
    char arr_4[len_2_constexpr];         // 合法

    // char arr_5[len_foo()+5];          // 非法
    char arr_6[len_foo_constexpr() + 1]; // 合法

    std::cout << fibonacci(10) << std::endl;
    // 1, 1, 2, 3, 5, 8, 13, 21, 34, 55
    std::cout << fibonacci(10) << std::endl;
    return 0;
}

上面的例子中,char arr_4[len_2] 可能比较令人困惑,因为 len_2 已经被定义为了常量。为什么 char arr_4[len_2] 仍然是非法的呢?这是因为 C++ 标准中数组的长度必须是一个常量表达式,而对于 len_2 而言,这是一个 const 常数,而不是一个常量表达式,因此(即便这种行为在大部分编译器中都支持,但是)它是一个非法的行为,我们需要使用接下来即将介绍的 C++11 引入的 constexpr 特性来解决这个问题;而对于 arr_5 来说,C++98 之前的编译器无法得知 len_foo() 在运行期实际上是返回一个常数,这也就导致了非法的产生。

注意,现在大部分编译器其实都带有自身编译优化,很多非法行为在编译器优化的加持下会变得合法,若需重现编译报错的现象需要使用老版本的编译器。

C++11 提供了 constexpr 让用户显式的声明函数或对象构造函数在编译期会成为常量表达式,这个关键字明确的告诉编译器应该去验证 len_foo 在编译期就应该是一个常量表达式。

此外,constexpr 修饰的函数可以使用递归:

constexpr int fibonacci(const int n) {
    return n == 1 || n == 2 ? 1 : fibonacci(n-1)+fibonacci(n-2);
}

从 C++14 开始,constexpr 函数可以在内部使用局部变量、循环和分支等简单语句,例如下面的代码在 C++11 的标准下是不能够通过编译的:

constexpr int fibonacci(const int n) {
    if(n == 1) return 1;
    if(n == 2) return 1;
    return fibonacci(n-1) + fibonacci(n-2);
}

 为此,我们可以写出下面这类简化的版本来使得函数从 C++11 开始即可用:


constexpr int fibonacci(const int n) {
    return n == 1 || n == 2 ? 1 : fibonacci(n-1) + fibonacci(n-2);
}

6. typedef

定义类型别名(type alias)采用传统方法就是使用关键字 typedef(type definition)

typedef double wages;       // wages 是double的别名或者说同义词
typedef double base, *p;    // base 是double的同义词, p是double * 的同义词

如果某个类型别名指代的是复合类型或常量,那么把它用到声明语句就会产生意想不到的结果。

比如下面的pstring,它的类型是char * 的别名

typedef char *pstring;    // pstring 是char*的别名或者说同义词
const pstring cstr = 0;   // cstr是指向char的常量指针
const pstring *ps;        // ps 是一个指针,它的对象是指向char的常量指针

注意:上面两条声明语句的基本数据类型都是const pstring,和过去一样,const是对给定类型的修饰。首先 pstring 是指针无疑,还是可以按照“就近原则”,const就近的是pstring,而pstring是指针类型,所以const修饰指针,指针是常量,pstring实际上指向的是char的指针,所以合起来就是pstring是一个指向char的常量指针,而非指向常量字符的指针(const没有修饰指向的对象)。

遇到上述复合类型别名的声明语句时,往往会错误地尝试把类型别名替换成它本来的样子。

const char *cstr = 0 ;    // 报错, 这是对const pstring cstr 的错误理解

按照这种替代后的理解那就是指向const char的普通指针,而不是原本的指向char的常量指针。

这种直接带入法在基本数据类型还可以,对于复合类型就很容易出错,避免错误的一个关键就在于可以沿用就近原则,确定const到底修饰的是哪一部分。

7. auto

使用auto关键字,可以让编译器去替我们分析表达式所属的类型,一般简单的类型最好自己写出具体类型,如果很难确定的变量类型或很长的名字,建议使用auto,因为使用auto后自然地降低程序的可读性和直观理解,遵循“无必要,不使用”原则。

auto让编译器通过初始值来推算变量的类型,所以auto定义的变量必须初始化,否则无从推算

使用auto能在一条语句中声明多个变量。因为一条声明语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型必须一致。

auto i =0, *p = &i;       // i是整数, p是指向整数的指针

auto sz =0, pi = 3.14;   // 错误, sz与 i 的类型不一致

8.dectype

decltype 类型说明符生成指定表达式的类型。 decltype 类型说明符与 auto 关键字一起,主要对编写模板库的开发人员有用。 使用 auto 并 decltype 声明其返回类型的函数模板取决于其模板参数的类型。 或者,使用 auto 并 decltype 声明一个函数模板,该模板包装对另一个函数的调用,然后返回包装函数的返回类型。

decltype( expression )

int a = 5, *pa = &a, &b = a;
decltype(a) c = a;    // c是int
decltype(b) d = a;    // d是int&,即引用
decltype(b+0) e = a;  // e是int,因为加法结果是int,不再是引用
decltype(*pa) f = a;  // f是int&,且必须初始化

注意:如果表达式的内容是解引用操作,decltype将得到引用类型。解引用得到的是指针所指的对象,而且还能给这个对象赋值,decltype(*pa)的结果类型就是 int&,而非int

decltype((variable)),这里使用的是双层括号,得到的永远都是引用,而decltype(variable)结果只有当variable本身是引用时才是引用。

9.预处理变量与预处理器、头文件保护符

9.1预处理变量与预处理器

C++过去的旧标准中采用 null 定义空指针,定义在头文件cstdlib,它的值是0.现在新标准改用nullptr。这里的null / nullptr 都是预处理变量。所谓预处理变量就是在程序开头定义一个全局量,预处理变量不属于命名空间std::,它由预处理器负责管理。使用预处理变量无须在前面加上std::。

也就是说预处理变量无视C++语言中关于作用域的规则。

当遇到一个预处理变量时,预处理器会自动将它替换为实际值。如下define定义的变量就是预处理变量。

#define MAX_LENGTH_ONE_LINE (10240)

// channel components
#define CHX 0
#define CHY 1
#define CHZ 2

把int变量直接赋值给指针是错误的操作,即使int变量的值刚好等于0也不行。

int *pi;
int zero = 0;
pi = zero; //错误,虽然都是0,当时类型不匹配
pi = 0; //正确

使用未经初始化的指针是引发错误的一大原因。如果访问了未经初始化的指针,则该指针所占内存空间当前内容就被当做一个地址值。访问该指针,就相当于去访问一个本不存在的位置上的本不存在的对象。

尽量等定义的对象确定后,再定义指向它的指针。如果实在不清楚指针应该指向何处,就把它初始化为nullptr或0,避免野指针出现。

9.2头文件保护符

头文件保护符依赖于预处理变量,下文中的SALES_DATA_H,一般是基于头文件类的名字来构建保护符的名字。预处理变量包括已定义和未定义。无论是否需要,都最好习惯性加上头文件保护符,一般预处理变量的名字全部大写

#ifndef SALES_DATA_H
#define SALES_DATA_H
#include <string>
struct Sales_data {

    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};
#endif

10.this

this 是 C++ 中的一个关键字,也是一个 const 指针,它指向当前对象,通过它可以访问当前对象的所有成员。

person所谓当前对象,是指正在使用的对象。例如对于person.name();,person就是当前对象,this 就指向 person。

this 只能用在类的内部,通过 this 可以访问类的所有成员,包括 private、protected、public 属性的。
注意,this 是一个指针,要用->来访问成员变量或成员函数。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

scott198512

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值