一、C++关键字
和C语言一样,用C++编写程序时也有很多应注意的关键字。了解更多的关键字有利于我们更好的去编写程序。
以下是C++98中的关键字目录,C++11中也会有相应的不同,具体这里不加赘述。具体大家可以下去了解了解。
一、命名空间
在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作 用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字 污染,namespace关键字的出现就是针对这种问题的。
1.命名空间定义
定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名 空间的成员。
应注意的是:命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该名命空间中。
2.命名空间的使用
命名空间的使用有三种方式:
(1)加命名空间及作用域限定符
(2)使用using将命名空间成员引入
(3)使用using namespace 命名空间引入
例:有如下命名空间
namespace N
{
int a=0;
char c='C';
}
使用方式一:加命名空间及作用域限定符
int main()
{
//c是命名空间N的一个成员
printf("%c",N::c);
return 0;
}
使用方式二:使用using将命名空间成员引入
using N::c;//放出成员c
int main()
{
//成员C被引入
printf("%c",c);
//成员a未被引入
printf("%d",a);
return 0;
}
使用方式三:使用using namespace 命名空间引入
using namespace N;//完全打开命名空间
int main()
{
printf("%d",a);
printf("%c",c);
return 0;
}
注:三中打开方式有所不同,注意区分。
3.C++ 的输入与输出
#include
using namespace std;
int main()
{
cout<<"Hellow world"<
说明:
#include
using namespace std;
int main()
{
int a;
char b;
double c;
//输入数据
cin>>a;
cin>>b;
cin>>c;
//输出数据
cout<
运行结果:
4.缺省参数
(1)缺省参数概念
缺省参数是声明或定义函数时为函数的参数指定一个默认值。在调用该函数时,如果没有指定实参则采用该 默认值,否则使用指定的实参。
void add(int a=1,int b=2)
{
return a+b;
}
int main()
{
add(5,6);//传参时,使用传入参数
add();//未传参时,采用默认参数
return 0;
}
(2)缺省参数的分类
1.全缺省参数
void print(int a=10,.int b=20,c=30)
{
cout<
2半缺省参数
void print(int a,int b=20,int c=30);
{
cout<
注意:
//在函数声明中给出
void print(int a=10);
//在函数定义时
void print(int a=30);
如上情况,那么在调用该函数时,如果未传入参数,那么编译器将不知道采用那一个默认值。
2. 缺省值必须是常量或者全局变量
3. C语言不支持(编译器不支持)
5.函数重载
1**.函数重载概念**:函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些**同名函数的 形参列表(参数个数 或 类型 或 顺序)**必须不同,常用来处理实现功能类似数据类型不同的问题。
例如:
int add(int a,int b)
{
return a+b;
}
double add(double a,double b)
{
return a+b;
}
int main()
{
add(1,2);
add(1.5,2.6);
return 0;
}
注意:
int add(int a,int b)
{
return a+b;
}
double add(int a,int b)
{
return a+b;
}
以上两个函数不时函数返回值地类型不同,但不属于函数重载。
道理含简单,函数参数类型数目都相同,那么在函数调用时编译器将不知道调用哪一个函数。所以应注意函数重载的含义。
2.名字修饰
在C/C++中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接。
Name Mangling是一种在编译过程中,将函数、变量的名称重新改编的机制,简单来说就是编译器为了区分各个函数,将函数通过某种算法,重新修饰为一个全局唯一的名称。
C语言的名字修饰规则非常简单,只是在函数名字前面添加了下划线。比如,对于以下代码,在后链接时就 会出错。
int Add(int left, int right);
int main()
{
Add(1, 2);
return 0;
}
编译器报错:error LNK2019: 无法解析的外部符号 _Add,该符号在函数 _main 中被引用。
上述Add函数只给了声明没有给定义,因此在链接时就会报错,提示:在main函数中引用的Add函数找不到 函数体。从报错结果中可以看到,C语言只是简单的在函数名前添加下划线。因此当工程中存在相同函数名的 函数时,就会产生冲突。
由于C++要支持函数重载,命名空间等,使得其修饰规则比较复杂,不同编译器在底层的实现方式可能都有差异。
double Add(double left, double right);
int Add(int left,int right);
int main()
{
Add(1, 2);
Add(1.0, 2.0);
return 0;
}
在vs下,对上述代码进行编译链接,后编译器报错:
error LNK2019: 无法解析的外部符号 “double cdecl Add(double,double)” (?Add@@YANNN@Z)
error LNK2019: 无法解析的外部符号 “int __cdecl Add(int,int)” (?Add@@YAHHH@Z)
通过上述错误可以看出,编译器实际在底层使用的不是Add名字,而是被重新修饰过的一个比较复杂的名字,被重新
修饰后的名字中包含了:函数的名字以及参数类型。这就是为什么函数重载中几个同名函数要求 其参数列表不同的原因。只要参数列表不同,编译器在编译时通过对函数名字进行重新修饰,将参数类型包 含在终的名字中,就可保证名字在底层的全局唯一性。
(3)extern" C"
有时候在C++工程中可能需要将某些函数按照C的风格来编译,在函数前加extern “C”,意思是告诉编译器, 将该函数按照C语言规则来编译。
extern "C" int Add(int left, int right);
int main()
{
Add(1,2);
return 0;
}
链接时报错:error LNK2019: 无法解析的外部符号_Add,该符号在函数 _main 中被引用
6.引用
(1)引用的概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
例如:你的名字和别人给你起的外号,叫法不同,但指的都是你自己。
定义一个引用:
类型& 引用变量名(对象名) = 引用实体
int main()
{
int a=10;
int& ra=a;//定义引用类型
printf("%p",&a);
printf("%p",&ra);
rerurn 0;
}
注意:引用类型必须和引用实体是同种类型
(2)引用的特性
void TestRef()
{
int a = 10;
// int& ra; // 该条语句编译时会出错
int& ra = a;
int& rra = a;
printf("%p %p %p\n", &a, &ra, &rra);
return 0;
}
(3)常引用
void TestConstRef()
{
const int a = 10;
//int& ra = a; // 该语句编译时会出错,a为常量
const int& ra = a;
// int& b = 10; // 该语句编译时会出错,10为常量
const int& b = 10;
double d = 12.34;
//int& rd = d; // 该语句编译时会出错,类型不同
const int& rd = d;
}
在这里要提到的是:给某非常变量的变量定义一个类型不同的常引用时是可以的,而给一个常变量定义一个类型不同的引用是不可以的。
原因是:给某非常变量的变量定义一个不同类型的常引用时,类型转换过程中生成一个临时值(临时值具有常性),故将其转换成一个常引用后其值仍不能被更改,因此是可以的。相反给一个常变量定义一个类型不同的引用,会导致,原数据从常变量变为一个非常变量,它的值将会被更改,这是不安全的。因此这是不可以的。
(4)使用场景
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
int& TestRefReturn(int& a)
{
a += 10;
return a;
}
看下面代码:
int& Add(int a, int b)
{
int c = a + b; return c;
}
int main()
{
int& ret = Add(1, 2);
Add(3, 4);
cout << "Add(1, 2) is :"<< ret <
运行结果:
为什么输出的值是7,而不是3呢?
注意:如果函数返回时,离开函数作用域后,其栈上空间已经还给系统,因此不能用栈上的空间作为引 用类型返回。如果以引用类型返回,返回值的生命周期必须不受函数的限制(即比函数生命周期长)。
(5)传值、传引用的效率比较
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。那指针和引用作为参数和返回值类型的效率呢?
#include
#include
using namespace std;
struct A
{
int a[10000];
};
A a;
A TestFunc1()
{
return a;
}
A& TestFunc2()
{
return a;
}
void TestRefAndValue()
{
// 以指针作为函数的返回值类型
size_t begin1 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc1();
size_t end1 = clock();
// 以引用作为函数的返回值类型
size_t begin2 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc2();
size_t end2 = clock();
// 分别计算两个函数运行结束后的时间
cout << "TestFunc1(int*)-time:" << end1 - begin1 << endl;
cout << "TestFunc2(int&)-time:" << end2 - begin2 << endl;
}
// 运行多次,检测指针和引用在传参方面的效率区别
int main()
{
for (int i = 0; i < 10; ++i)
{
TestRefAndValue();
}
return 0;
}
运行结果:
通过上述代码的比较,发现引用和指针在作为传参以及返回值类型上效率几乎相同。
(6)引用和指针的不同
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
在底层实现上实际是有空间的,因为引用是按照指针方式来实现的
int main()
{
int a=10;
int& ra=a;
int*pa=&a;
*pa=20;
return 0;
}
int& Add(int a, int b)
{
int c = a + b; return c;
}
int main()
{
int ret =Add(3, 4);
cout << ret <
如果在上述函数前增加inline关键字将其改成内联函数,在编译期间编译器会用函数体替换函数的调用。
查看方式:
说到代码的替换我们会想到宏定义,我们知道一个程序在预编译阶段会进行宏的替换,那么宏与内联函数相比有哪些优缺点呢?
优点:
1.增强代码的复用性。
2.提高性能。
缺点:
1.不方便调试宏。(因为预编译阶段进行了替换)
2.导致代码可读性差,可维护性差,容易误用。
3.没有类型安全的检查
C++有哪些技术替代宏?
int void Testauto()
{
return 10;
}
int main()
{
int a=10;
auto b=a;
auto c='a';
auto d=Testauto();
cout<
【注意】
使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类 型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为 变量实际的类型。
(2) 在同一行定义多个变量 当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对 第一个类型进行推导,然后用推导出来的类型定义其他变量。
void TestAuto()
{
auto a = 1, b = 2;
auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}
(3)auto不能推导的场景
(1) auto不能作为函数的参数
// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{}
(2)auto不能直接用来声明数组
void TestAuto()
{
int a[] = {1,2,3};
auto b[3] = a;
}
(3 )为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法。
(4)auto在实际中常见的优势用法就是跟以后会讲到的C++11提供的新式**for循环,**还有lambda表达式等 进行配合使用。
int main()
{
int a[]={0,1,2,3,4,5,6};
for(auto i:a)
{
cout<
(5) auto不能定义类的非静态成员变量(目前了解就行,后面学习)
(6)实例化模板时不能使用auto作为模板参数(目前了解就行,后面学习)
9. 基于范围的for循环(C++11)
在C++98中如果要遍历一个数组,可以按照以下方式进行:
void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
array[i] *= 2;
for (int* p = array; p < array + sizeof(array)/ sizeof(array[0]); ++p)
cout << *p << endl;
}
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中 引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量, 第二部分则表示被迭代的范围。
int main()
{
int array[] = { 1, 2, 3, 4, 5 };
for(auto& e : array)
e *= 2;
for(auto e : array)
cout << e << " ";
return 0;
}
注意:与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环。
10. 指针空值nullptr(C++11)
(1)C++98中的指针空值
在良好的C/C++编程习惯中,声明一个变量时好给该变量一个合适的初始值,否则可能会出现不可预料的 错误,比如未初始化的指针。如果一个指针没有合法的指向,我们基本都是按照如下方式对其进行初始化:
int main()
{
int* p1=NULL;
int* p2=0;
return 0;
}
NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码
#ifndef NULL
#ifdef_cplusplus
#define NULL 0
#else
#define NULL ((void*)0)
#endif
#denif
可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如:
void f(int)
{
cout<<"f(int)"<
程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的初衷相悖。 在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下 将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。
(2)nullptr 与 nullptr_t
为了考虑兼容性,C++11并没有消除常量0的二义性,C++11给出了全新的nullptr表示空值指针。C++11为什 么不在NULL的基础上进行扩展,这是因为NULL以前就是一个宏,而且不同的编译器厂商对于NULL的实现可 能不太相同,而且直接扩展NULL,可能会影响以前旧的程序。因此:为了避免混淆,C++11提供了 nullptr,即:nullptr代表一个指针空值常量。nullptr是有类型的,其类型为nullptr_t,仅仅可以被隐式转 化为指针类型,nullptr_t被定义在:
namespace std
{
typedef decltype(nullptr) nullptr_t;
}
注意: