C++相关基础特性了解
空指针
空指针(null pointer)不指向任何对象,在试图使用一个指针之前代码可以首先检查它是否为空。
以下列出几个生成空指针的方法:
int *p1 = nullptr; // 等价于int *p1 = 0
int *p2 = 0; // 直接将p2初始化为字面常量0
// 需要首先#include<cstdlib>
int *p3 = NULL; // 等价于int *p3 = 0;
得到空指针最直接的方法就是用字面值nullptr来初始化指针,这也是C++11新标准刚刚引入的一种方法。nullptr是一种特殊类型的字面值,它可以被转换成任意其他的指针类型。
注:NULL预处理变量 — 位于头文件cstdlib
过去的程序会用到一个名为NULL的预处理变量(preprocesspr variable)来给指针赋值,这个变量在头文件cstdlib中定义,它的值就是0
小知识点:预处理器是运行于编译过程之前的一段程序
预处理变量不属于命名空间std,它由预处理器负责管理,因此我们可以直接使用预处理变量而无须在前面加上std::
当用到一个预处理变量时,预处理器会自动地将它替换为实际值,因此用NULL初始化指针和0初始化指针是一样的。在新标准下,现在的C++程序最好使用nullptr,同时避免使用NULL
注意事项:把int变量直接赋给指针是错误的操作,即使int变量恰好等于0也不行
int zero = 0;
int *p = zero; // 错误:不能把int变量直接赋给指针
建议:初始化所有指针(使用初始化的指针)
使用未经初始化的指针是引发运行时错误的一大原因。
和其他变量一样,访问未经初始化的指针所引发的后果也是无法预计的。通常这一行为将造成程序崩溃,而且一旦崩溃,想要定位到出错位置将是特别麻烦的问题。
在大多数编译器环境下,如果使用了未经初始化的指针,则该指针所占内存空间的当前内容将被看作一个地址值。访问该指针,相当于去访问一个本不存在的位置上的本不存在的对象。糟糕的是,如果指针所占内存空间中恰好有内容,而这些内容又被当作了某个地址,我们就很难分清它到底是合法还是非法的了。
**因此建议初始化所有的指针,并在可能的情况下,尽量等定义了对象之后再定义指向它的指针。**如果实在不清楚指针应该指向何处,就把它初始化为nullptr或者0,这样程序就能检测并知道它没有指向任何具体的对象了。
处理类型
1.类型别名
类型别名(type alias)是一个名字,它是某种类型的同义词。使用类型别名有很多好处,它让复杂的类型名字变得简单明了,易于理解和使用,还有助于程序员清楚地知道使用改类型的真实目的
有两种方法可以用于定义类型别名。
传统的方法是使用关键字typedef:
typedef double dou; // dou是double的同义词
typedef int I; // I是int的同义词
typedef long long ll; // ll是long long的同义词
其中,关键字typedef作为声明语句中的基本数据类型的一部分出现。含有typedef的声明语句定义的不再是变量而是类型别名。和以前的声明语句一样,这里的声明符也可以包含类型修饰,从而也能由基本数据类型构造出复合类型来。
新标准规定了一种新的方法,使用别名声明(alias declaration)来定义类型的别名:
using stu = Student; // stu是Student的同义词
这种方法用关键字 using 作为别名声明的开始,其后紧跟别名和等号,其作用是把等号左侧的名字规定成等号右侧类型的别名
类型别名和类型的名字等价,只要是类型的名字能出现的地方,就能使用类型别名:
dou price,salary; // 等价于 double price,salary;
stu s1; // 等价于 Student s1;
2.auto类型说明符
结论:(auto定义的变量必须有初识值)
编程时我们常常需要把表达式的值赋给变量,这就要求在声明变量的时候清楚地知道表达式的类型。但是,要做到这一点并非那么容易,有时可能根本做不到
C++11新标准引入了auto类型说明符,用它就能让编译器替我们去分析表达式所属的类型。和原来那些只对应一种特定类型的说明符(比如double)不同,auto让编译器通过初始值来推算变量的类型。显然,auto定义的变量必须有初始值:
// 由val1和val2相加的结果可以推断出item的类型
auto item = val1 + val2; // item初始化为val1和val2相加的结果
此处编译器将根据 val1 和 val2 相加的结果来推断item的类型。
使用auto也能在一条语句中声明多个变量。因为一条语句只能有一个基本数据类型,所以改语句中所有变量的初始基本数据类型都必须一样:
auto i = 0,*p = &i; // 正确:i是整数,p是整型指针
auto sz = 0,pi = 3.14; // 错误:sz和pi的类型不一致
3.decltype类型指示符(仅作了解)
少数情况:希望从表达式的类型推断出要定义的变量的类型,但是不想用表达式的值初始化变量。
C++11新标准引入了第二章类型说明符decltype,它的作用是选择并返回操作数的数据类型。在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值:
#include<iostream>
using namespace std;
int abs(int a) {
return (a>=0) ? a:-a;
}
int main()
{
decltype(abs(-5)) val = 5;
cout<<val<<endl;
return 0;
}
4.编写头文件
头文件通常包含那些只能被定义一次的实体,比如类,const 和 constexpr 变量等。头文件也经常用到其他头文件的功能。
注:头文件一旦改变,相关的源文件必须重新编译以获取更新过的声明
预处理器概述
确保头文件多次包含仍然能安全工作的常用技术是预处理器(preprocessor),它由C++语言从C语言继承而来。预处理器是在编译之前执行的一段程序 ,可以部分地改变我们所写的程序。之前已经用到了一项预处理功能#include,当预处理器看到#include标记时就会用指定的头文件的内容代替#include
C++程序还会用到一项预处理功能的是头文件保护符(header guard),头文件保护符依赖于预处理变量。预处理变量有两种状态:已定义和未定义。
#define 指令把一个名字设定为预处理变量,另外两个指令则分别检查某个指定的预处理变量是否已经定义:#ifdef 当且仅当变量已定义时为真,#ifndef 当且仅当变量未定义时为真。一旦检查结果为真,则执行后续操作直至遇到 #endif 指令为止
而使用这些功能就能有效地防止重复包含的发生:
#ifndef STUDENT_H
#define STUDENT_H
#include<string>
struct Student { // 结构体定义形式
Student() =default;
std::string name;
unsigned height;
};
class student { // 类的定义形式
public:
student() =default;
~student() =default;
private:
std::string name;
unsigned height;
};
#endif
注意:预处理变量无视C++语言中关于作用域的规则
整个程序中的预处理变量包括头文件保护符必须唯一,通常的做法是基于头文件中类的名字来构建保护符的名字,以确保其唯一性。为了避免与程序中的其他实体发生名字冲突,一般把预处理变量的名字全部大写
建议:头文件即使(目前还)没有被包含在任何其他头文件中,也应该设置保护符。头文件保护符很简单,程序员只要习惯性地加上就可以了,没有必要太在乎你的程序到底需要不需要
标准库initializer_list类型(含有可变形参)- 拓展内容
为了编写能处理不同数量实参的函数,C++11新标准提供了两种主要的方法:如果所有的实参类型相同,可以传递一个名为initializer_list的标准库类型;如果实参的类型不同,我们可以编写一种特殊的函数,也就是所谓的可变参数模板,即tuple类型
initializer_list形参
如果函数的实参数量未知但是全部实参的类型都相同,我们可以使用initializer_list类型的形参。initializer_list是一种标准库类型,用于表示某种特定类型的值的数组。initializer_list类型定义在同名的头文件中。
注:initializer_list类型形参——函数的实参数量未知但全部形参类型都相同
initializer_list提供的操作:
操作 | 解释 |
---|---|
initializer_list< T > lst; | 默认初始化:T类型元素的空列表 |
initializer_list< T > lst{a,b,c…} | lst的元素数量和初始值一样多;lst的元素对应初始值的副本;列表中的元素是const |
lst2(lst) lst2 = lst | 拷贝或赋值一个initializer_list对象不会拷贝列表中的元素;拷贝后,原始列表和副本共享元素 |
lst.size() | 列表中的元素数量 |
lst.begin() | 返回指向lst中首元素的指针 |
lst.end() | 返回指向lst中尾元素下一位置的指针 |
和vector一样,initializer_list也是一种模板类型。定义initializer_list对象时,必须说明列表中所含元素的类型:
initializer_list<string> ls; // initializer_list的元素类型是string
initializer_list<int> li; // initializer_list的元素类型是int
但,和vector不一样的是,initializer_list对象中的元素永远是常量值,我们无法改变initializer_list对象中元素的值!!!
例如:我们使用如下的形式编写输出错误信息的函数,使其可以作用于可变数量的实参:
void error_msg(initializer_list<string> il) {
for(auto beg=il.begin();beg!=il.end();++beg)
cout<<*beg<<ends;
cout<<endl;
}
完整程序代码:
#include<iostream>
#include<initializer_list>
#include<string>
using namespace std;
void error_msg(initializer_list<string> il) {
for(auto beg=il.begin();beg!=il.end();++beg)
cout<<*beg<<ends;
cout<<endl;
}
int main()
{
error_msg({"run_error","Memory_leak"});
return 0;
}
**练习:**编写一个程序,它的参数是initializer_list< int >类型的对象,函数的功能是计算列表中所有元素的和
#include<iostream>
#include<initializer_list>
#include<string>
using namespace std;
int iCount(initializer_list<int> il) {
int count = 0;
// 遍历il的每一个元素
for(auto val:il)
count += val;
return count;
}
int main()
{
// 使用列表初始化的方式构建initializer_list<int>对象
// 然后把他作为实参传递给函数iCount
cout<<"1,6,9的和:"<<iCount({1,6,9})<<endl;
cout<<"4,5,9,18的和:"<<iCount({4,5,9,18})<<endl;
return 0;
}