一、简介
Day03,C11入门
二、主要内容
1. 名字空间域
在C++中支持三种域:局部域、名字空间域和类域。
名字空间域是随着标准C++而引入的,它相当于一个更加灵活的文件域(全局域),可以用花括号把文件的一部分括起来,并以关键字namespace开头给它起个名字。
a. 定义方式
如下所示,定义名为tmp的名字空间域,
namespace tmp
{
int g_max = 10;
int g_min = 0;
char ch = 'a';
void fun()
{
// 函数体
}
int my_add(int a, int b)
{
return a + b;
}
}
其主要是为了解决全局名字污染问题,即防止程序中全局实体名与其他程序的全局实体名的命名冲突。
b. 使用方法
如下所示,
namespace A
{
int g_max = 10;
int g_min = 0;
int my_add(int a, int b)
{
return a + b;
}
}
namespace B
{
double g_max = 12.23;
double g_min = 0.0;
double my_add(int a, int b)
{
return a + b;
}
}
int main()
{
int a = A::my_add(1,5);
int b = A::g_max;
double dx = B::my_add(12.566, 546.15);
return 0;
}
在命名空间A和B中,分别定义了my_add()函数,在使用时需通过作用域限定符"::"来强制调用该命名空间的my_add()函数。
或在程序前使用using namespace 名字空间名来强制程序在该名字空间下执行程序,如下,
int main()
{
using namespace A;
cout << g_max << endl; // 10
cout << g_min << endl; // 0
return 0;
}
程序执行后会输出10,0。
c. 注意事项
若在不同命名空间中定义了相同的函数,且都使用using namespace 名字空间名限定空间,则在程序调用任一函数时会产生二义性,即程序无法判断使用哪一个名字空间的函数,此时需要作用域限定符"::"。
2. new/delete
C++的动态内存管理关键字,类似于C语言中的malloc、calloc、realloc以及free函数(在stdlib.h库中)。
a. C中的动态内存管理
使用stdlib.h库中的malloc、calloc、realloc以及free函数来进行动态内存管理。
int main()
{
int n = 10;
// 申请一个整形空间
int* ip1 = (int*)malloc(sizeof(int));
// 申请连续的n个整形空间
int* ip2 = (int*)malloc(sizeof(int) * n);
// 申请连续的n个空间,并赋值为0
int* ip3 = (int*)calloc(n, sizeof(int));
// 将ip2大小扩大到2*n
ip2 = (int*)realloc(ip2, sizeof(int) * n * 2);
// 释放全部申请的空间
free(ip1);
free(ip2);
free(ip3);
// 释放后需要将其指向空,否则其会成为失效指针
ip1 = nullptr;
ip2 = nullptr;
ip3 = nullptr;
return 0;
}
b. C++中的动态内存管理
使用关键字new/delete来进行管理,new表示申请空间,delete表示释放空间。
int main()
{
int n = 10;
// 申请一个整形空间,并初始化为10
int *ip1 = new int{ 10 };
// 申请连续的n个整形空间,不进行初始化
int *ip2 = new int[n];
// 申请连续的n个整形空间,并将所有空间初始化(默认为0)
int *ip3 = new int[n]{ };
/*在初始化时,所给出的个数需小于等于指定的空间大小*/
// 释放空间
// 若为一个空间,则如下所示释放即可
delete ip1;
// 若为多个空间,则需如下所示释放,表示释放所有空间
delete[] ip2;
delete[] ip3;
// 仍需指向为空,否则为野指针
ip1 = NULL;
ip2 = NULL;
ip3 = NULL;
}
c. new/delete的函数化使用
在底层中,operator new实际上调用malloc()函数来申请空间。
int main()
{
int n = 10;
// 如下方式类似于 malloc(sizeof(int))
int* ip1 = (int*)::operator new(sizeof(int));
// 如下方式类似于 malloc(sizeof(int) * n)
int* ip2 = (int*)::operator new(sizeof(int) * n);
// free(ip1)
::operator delete(ip1);
::operator delete[](ip2);
//仍需指向为空
ip1 = NULL;
ip2 = NULL;
return 0;
}
d. 定位new的使用
用于对申请的空间的初始化
int main()
{
int n = 10;
int* ip1 = (int*)malloc(sizeof(int));
int* ip2 = (int*)::operator new(sizeof(int) * n);
// ip1、ip2都是仅申请空间,但未初始化
// 对ip1指向的空间进行初始化,值为10
new(ip1) int { 10 };
// 此方式会报错,int代表仅初始化一个空间
// new(ip2) int { 1,2,3,4,5 }; // error
// 此方式正确,但仅会初始化5个空间,若为int[n],则其他空间会初始化为0(默认值)
new(ip2) int[] { 1,2,3,4,5 };
free(ip1);
ip1 = NULL;
::operator delete[](ip2);
ip2 = NULL;
return 0;
}
e. 区别
对于内置类型new / delete / malloc/free可以混用。区别如下,
1. new/delete 是C++中的运算符。malloc/free是函数。
2. malloc申请内存空间时,手动计算所需大小,new只需类型名,自动计算大小;
3. malloc申请的内存空间不会初始化,new可以初始化;
4. malloc的返回值为void*,接收时必须强转,new不需要;
5. malloc申请内存空间失败时,返回的是NULL,使用时必须判空;
new申请内存空间失败时抛出异常,所以要有捕获异常处理程序;
3. C11新特性
a. 类型推导:auto/decltype
C++11引入了auto和decltype关键字实现类型推导,通过这两个关键字不仅能方便的获取复杂类型,而且还能简化书写,提高编码效率。
1) auto
auto类型推导:auto定义的变量,可以根据初始化的值,在编译时推导出变量名的类型。
int main()
{
auto x = 10; // 10 -> x = int, auto = int
auto dx = 12.23; // 默认为双精度即double类型,auto = double
auto df = 12.23f; // 指定该数据类型为float类型,auto = float
auto ch = 'a'; // auto = char
}
复杂情况(指针)的推演情况如下所示,
int main()
{
auto x = 5; // auto = int
const auto* xp = &x; // auto = const int
auto ip = &x; // auto = int*
auto* sp = &x; // auto = int
// *和&都是和变量名结合 并不与关键字结合
const int a = 10;
auto* p = &a; //auto = const int
return 0;
}
auto总结:
auto并不能代表一个实际的类型声明,只是一个类型声明的"占位符",在实际使用中,必须给予auto声明的变量以初始值,以让编译器推断出它的实际类型,并在编译中将auto占位符替换为真正的数据类型;
不能推演数组,即auto ar[10] = { 1,2,3,4 };会报错;
在结构体中,成员变量不能使用auto来声明;
2) decltype
decltype类型推导,用于在编译时推导出一个表达式的类型。
语法格式:decltype(exp),其中exp表示一个表达式,从格式上来看,decltype很像sizeof——用于推导表达式类型大小的操作符,如下,
int main()
{
int x = 10;
decltype(x) y = 1; // y = int
decltype(x + y) z = 10; // z = int
double dx = 10;
decltype(x + dx) c; // c = double
const int& a = 10;
decltype(a) j = 10; // j = const int&
int m = 10;
const decltype(m)* p = &m; // p = const int *
decltype(m)* ip = &m; // ip = int *
const int n = 10;
decltype(n) k = n; // k = const int
reutrn 0;
}
b. 基于范围的for循环
在C98中,不同的容器和数组,遍历的方法不尽相同,写法不统一,也不够简洁,而C++11基于范围的for循环以统一、简洁的方式来遍历容器和数组,用起来更方便。
1) C语言中的for循环
在C语言中,遍历数组有如下两种方式,
int main()
{
int ar[] = { 1,2,5,6,7,8,9,5,3,4 };
int n = sizeof(ar) / sizeof(ar[0]);
for(int i = 0; i < n; i++)
cout << ar[i] << " ";
int* ip = NULL;
for(ip = ar; ip != ar + n; ip++)
cout << *ip << " ";
return 0;
}
2) C++中基于范围的for循环
在C++中,基于范围的for循环的一般格式:
for(ElemType val: array)
{
// 循环体
}
ElemType:是范围变量的数据类型。它必须与数组(容器)元素的数据类型一样,或者是数组元素可以自动转换过来的类型;
val:是范围变量的名称。该变量将在循环迭代期间依次接收数组中的元素值。在第一次循环迭代期间它接收的是第一个元素的值;在第二次循环迭代期间,它接收的是第二个元素的值;以此类推;
array: 是要让该循环进行处理的数组(容器)的名称。该循环将对数组中的每个元素迭代一次;
statement: 是在每次循环迭代期间要执行的语句。要在循环中执行更多的语句,则可以使用一组大括号来包围多个语句。与其他循环体一样,可以用continue来结束本次循环,也可以用break来跳出整个循环。
int main()
{
int ar[] = { 1,2,5,6,7,8,9,5,3,4 };
// 1
for(int x : ar)
{
cout << x << " ";
x += 10; // 不影响数组中的值,因为仅是将数组中的值传递过来
}
// 遍历结果 1 2 5 6 7 8 9 5 3 4
cout << endl;
// 2
for(int x : ar)
cout << x << " ";
// 遍历结果 1 2 5 6 7 8 9 5 3 4
cout << endl;
return 0;
}
如上所示的代码中,第一个循环和第二个循环的结果都是相同的,对于x += 10;在迭代过程中,仅是将ar中的值传递过来,并不会改变实参的值,类似于函数的形参。但若以如下方式遍历数组,会得到不同的遍历结果。
int main()
{
int ar[] = { 1,2,5,6,7,8,9,5,3,4 };
// 1
for(int& x : ar)
{
cout << x << " ";
x += 10;
}
// 遍历结果 1 2 5 6 7 8 9 5 3 4
cout << endl;
// 2
for(int x : ar)
cout << x << " ";
// 遍历结果 11 12 15 16 17 18 19 15 13 14
cout << endl;
return 0;
}
在上述循环一中,每次迭代将x命名为各空间的别名,即取得了各空间的地址,
因此在x += 10后会影响个空间的值。
也可使用auto来进行遍历数组,如下所示,
int main()
{
int ar[] = { 1,2,3,4,5,6,7,8,9 };
char str[] = { 'a', 'b', 'c', 'd'};
double dx[] = { 1.2, 2.3, 3.4, 4.5 };
for(const auto& x : ar)
cout << x << " ";
cout << endl;
for(const auto& x : str)
cout << x << " ";
cout << endl;
for(const auto& x : dx)
cout << x << " ";
cout << endl;
return 0;
}
结合之前所学习的知识点,可得出如下结论,
int main()
{
int ar[] = { 1,2,3,4,5,6,7,8,9 };
int* ip = ar;
int(&br)[] = ar;
auto &cr = ar;
// 仅cr可通过基于范围的for循环进行遍历,ip和br都不可以
// ip仅是一个指针,br无法确定数组的大小
for(auto x : cr)
cout << x << " ";
cout << endl;
return 0;
}
c. 指针空值——nullptr:"指针空值类型"的常量
初始化指针是将其指向一个"空"的位置,比如0。但在大多数计算机系统中,并不允许用户程序写地址为0的内存空间,倘若程序无意中对该指针所指地址赋值,通常在运行时就会导致程序退出。虽然程序退出并非什么好事,但这样的错误也是十分明显的,容易进行修改,因此在大多数的代码中,常常使用指针初始化为0或NULL的语法。如下所示为在C/C++中对NULL的定义,
#ifndef NULL
#ifdef cplusplus
#define NULL 0 // C++
#else
#define NULL ((void*)0) // C
#endif
#endif
1) C语言中的空指针
由上述可知,在C语言中,对于NULL是将0强转为无类型指针。由于C语言中对指针类型的约束较少,因此将指针指向为NULL是成立的,即
int main()
{
int a = 10;
void* vp = &a;
// C编译器会自动将vp强转为int*指针
int* ip = vp;
// C编译器会自动将vp强转为char*指针
char* cp = vp;
return 0;
}
2) C++中的空指针
由上述可知,在C++中。仅将NULL定义为0值。这是由于C++中对指针类型的约束较强,即
int main()
{
int a = 10;
void* vp = &a;
// C++ 不允许该语法
// int* ip = vp; // error
// C++ 不允许该语法
// char* cp = vp; // error
// 仅该种语法成立
int* ip = (int*)vp;
char* cp = (char*)vp;
return 0;
}
为了解决C++中NULL定义为0的不方便之处,C11标准提出了nullptr,即指针空值常量,其可以赋值给任意类型的指针,且仅能赋值给指针类型变量,不能被修改,可通过强制类型转换赋值给普通变量,可通过下述代码得到更直观的表现。
void fun(int a)
{
cout << "fun(int a)" <<endl;
}
void fun(char* p)
{
cout << "fun(char* p)" <<endl;
}
int main()
{
fun(0); // fun(int a)
fun(nullptr); // fun(char* p)
return 0;
}
Tips:在未来的C++编写中,只使用nullptr,摒弃NULL的使用。
d. typedef和using
typedef和using是给予已有的类型名一个替代名称。 定义方式如下,两者作用都是一样的。
typedef unsigned char u_int8;
typedef unsigned short u_int16;
using u_int8 = unsigned char;
using u_int16 = unsigned short;
但在实际应用中,using更加灵活,最明显的特点是其可以和模板进行联系,如下所示,
template<class _T>
using pointer = _T*;
int main()
{
int a = 10;
pointer<int> p = &a;
char ch = 'a';
pointer<char> cp = &ch;
return 0;
}
Tips:在大部分源码中,都是以如上所示的方式定义的。
e. 左、右、将亡值
C11中,左值是指拥有地址的变量,右值是指常量(即没有地址),将亡值是指在函数调用中返回的值,其地址已被销毁。
意义在于可以通过左值/右值引用来进行函数重载,即不同的左值/右值引用也会构成函数重载,即
void fun(int& a)
{
cout << "left" << endl;
}
void fun(int&& a)
{
cout << "right" << endl;
}
f. string字符串(简单使用)
C中的字符串函数库:string.h / cstring。C语言中的字符串操作比较复杂,即操作字符串数组char str[],各种操作函数可参考相关资料。
在C++中,引入了string类型,库函数为string,值得注意的是其仅在std命名空间下使用,即必须使用using namespace std;
1) 定义方式
声明及定义方式如下所示,
int main()
{
// 声明与定义
string s1{ "test" };
string s2 = { "hello" }
string s3 = "world";
string s4 = s1;
cout << s1 <<endl;
return 0;
}
2) 使用方法
// 使用上述定义
int main()
{
// 组合/加长
s1 += s2;
cout << s1 <<endl;
// 判断是否相等
if(s1 == s2)
cout << "=" <<endl;
else
cout << "!=" <<endl;
// 获取长度
int len = s1.length();
// 外部输入
string s5;
cin >> s5;
cout << s5 << endl;
return 0;
}
值得注意的是,在进行外部输入时,若输入的字符串中包括空格,则系统仅接收空格之前的字符串,其之后(包括空格)都会被忽略,即
int main()
{
string s6;
// 输入:hello world.
cin >> s6;
cout << s6 << endl;
// 输出:hello
return 0;
}
这种情况的解决方法为使用全局函数getline(),如下所示,
int main()
{
string s6;
// getline()函数以回车(换行符作为输入结束)
getline(cin, s6); // 输入hello world
cout << s6 << endl; // 输出hello world
return 0;
}
Tips:相比与C语言对字符串的操作,C++的操作更为方便和简洁。