此篇为C++入门首篇,学习的主要是:命名空间 || C++输入和输出 || 缺省参数 || 函数重载 || 引用 || extern "C" || 内联 || auto关键字(C++11) || 基于范围的for循环(c++11) || 指针空值nullptr(C++)
等内容,此次让小新带领大家一起走向C++第一步
在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。 定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名 空间的成员。
#include
int rand = 0;
int main()
{
printf("hello yongheng\n");
printf("%d\n,rand");
}
#define _CRT_SECURE_NO_WARNINGS 1
#include
#include
int main()
{
printf("hello yongheng\n");
printf("%d\n",rand);
}
#define _CRT_SECURE_NO_WARNINGS 1
#include
#include
int rand = 0;
int main()
{
printf("hello yongheng\n");
printf("%d\n",rand);
}
此时的良方就是运用命名空间,因为它解决的就是命名冲突问题,namespace中的rand还是全局变量,此时namespace的作用就是为了防止stdlib库中的函数rand与变量rand命名冲突
#define _CRT_SECURE_NO_WARNINGS 1
#include
#include
namespace yongheng
{
int rand = 0;
}
int main()
{
printf("hello yongheng\n");
printf("%d\n",rand);
}
#define _CRT_SECURE_NO_WARNINGS 1
#include
#include
namespace yongheng
{
int rand = 0;//变量
int Add(int left,int right)//函数
{
return left + right;
}
struct Node //结构体
{
struct Node* next;
int val;
};
}
int main()
{
printf("%d",yongheng::rand);
yongheng::Add(1,2);
struct yongheng::Node node;
}
#define _CRT_SECURE_NO_WARNINGS 1
#include
#include
namespace N1
{
namespace N2
{
int Add(int left,int right)
{
return left + right;
}
}
}
int main()
{
printf("%d",N1::N2::Add(1,2)); // 3
}
注意:一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中
//单独展开某一项,用于展开命名空间中常用的
using N1::N2::Node;
int main()
{
struct Node node;
}
iostream库包含了输入输出流,std是一个标准库命名空间,里面包含了cout(流插入)和cin(流提取)等
①将std命名空间展开(适合练习的时候使用)
#define _CRT_SECURE_NO_WARNINGS 1
#include
using namespace std;//
int main()
{
cout << "hello world" << endl;
return 0;
}
②std命名空间中包含cout与cin和endl等,所以可以指定展开
#define _CRT_SECURE_NO_WARNINGS 1
#include
int main()
{
std::cout << "hello world" << std::endl;
return 0;
}
③将常用的展开
#define _CRT_SECURE_NO_WARNINGS 1
#include
using std::cout;
using std::endl;
int main()
{
cout << "hello world" << endl;
return 0;
}
#define _CRT_SECURE_NO_WARNINGS 1
#include
using namespace std;
int main()
{
int i = 0;
int j = 3.14;
cout << i << ' ' << j << endl;
cin >> i >> j;
cout << i << ' ' << j << endl;
}
说明:1. 使用 cout 标准输出 ( 控制台 ) 和 cin 标准输入 ( 键盘 ) 时,必须 包含 < iostream > 头文件 以及 std 标准命名空间。注意:早期标准库将所有功能在全局域中实现,声明在 .h 后缀的头文件中,使用时只需包含对应头文件即可,后来将其实现在 std 命名空间下,为了和 C 头文件区分,也为了正确使用命名空间,规定 C++ 头文 件不带 .h ;旧编译器 (vc 6.0) 中还支持格式,后续编译器已不支持,因此 推荐 使用 +std 的方式。2. 使用 C++ 输入输出更方便,不需增加数据格式控制,比如:整形 --%d ,字符 --%c
上图可以看出,如果只输出数值的话,还是C++更方便一些,C语言保留了6位小数。而如果结构体student中的成员越多的话,还是C语言的输出语句更方便一些,所以C语言和C++哪个更方便就可以选用哪个。
缺省参数是 声明或定义函数时 为函数的 参数指定一个默认值 。在调用该函数时,如果没有指定实参则采用该默认值,否则使用指定的实参。
全缺省参数: 即上述a,b,c三个都有缺省参数
需要注意的是:1. 半缺省参数必须 从右往左依次 来给出,不能间隔着给2. 缺省参数不能在函数声明和定义中同时出现//a.h void TestFunc(int a = 10); // a.c void TestFunc(int a = 20) {} // 注意:如果生命与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那 个缺省值。
3. 缺省值必须是常量或者全局变量4. C 语言不支持(编译器不支持)
函数重载 : 是函数的一种特殊情况, C++允许在同一作用域中声明几个功能类似的同名函数 , 这些同名函数的形参列表(参数个数 或 类型 或 顺序)必须不同 ,常用来处理实现功能类似数据类型不同的问题
首先我们先建立一个头文件 fac.h,再建立俩个文件fac.c和test.c ,然后我们我们回顾一下编译器编译的整个过程:
1. 预处理: 进行的是头文件展开,宏替换,条件编译,去掉注释等等 生成fac.i 和 test.i
2. 编译: 进行检查语法,生成汇编代码 生成fac.s 和 test.s
3. 汇编: 汇编代码转换成二进制机器码 生成fac.o 和 test.o
4. 链接: 生成a.out① c语言不支持函数重载:因为在编译的时候,俩个重载函数,函数名相同,在fac.o符号表中存在歧义和冲突,其次链接的时候也存在歧义和冲突,因为它们都是直接使用函数名去标识和查找的,而重载函数,它们的函数名又是相同的,所以不支持重载
②c++支持重载函数:因为C++的目标文件符号表中不是直接用函数名来表示和查找的,而是使用函数名修饰规则去查找(在不同编译器下不同),有了这个函数名修饰规则,只要参数不同,fac.o符号表里面重载的函数就不存在二义性和冲突了,所以在链接的时候,test.o的main函数里面去调用这俩个重载的函数,查找他们的地址时,也是非常明确,且没有歧义的
需要注意的是:
①如果当前文件有函数定义,那么在编译的时候就把地址填上了
②如果在当前只有函数的声明,那么定义肯定是在其他的.cpp中,编译时是没有地址的,只能在链接的时候去其他的.o符号表中根据函数名修饰规则去找,这是链接部分所做的
③函数名修饰规则: _z + 函数名长度 + 函数名 + 参数首字母 如 函数名 f() : _z1fv
引用 不是新定义一个变量,而 是 给已存在变量取了一个别名 , 编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
比如: 李逵 ,在家称为 " 铁牛 " ,江湖上人称 " 黑旋风。
类型& 引用变量名(对象名) = 引用实体
d是a的小名,那就不能有一个人的大名叫d,即同一个域中不能有相同的名字
指针可以看作是一个渣男,第一次喜欢A,第二次又喜欢B. 而引用一旦喜欢上A,他就不可能喜欢B了
//传指针
void Swap(int* p1,int* p2)
{
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
//传引用,引用做参数,把a,b起个别名
void Swap(int& r1,int& r2)
{
int temp = r1;
r1 = r2;
r2 = temp;
}
//传值
void Swap(int r1, int r2)
{
int temp = r1;
r1 = r2;
r2 = temp;
}
//他们三个构成重载(类型不同)
//但是Swap(a,b);调用是存在歧义,他们不知道调用,传值还是传引用
int main()
{
int a = 10, b = 20;
Swap(&a,&b);
/*Swap(a,b);*/
}
注意: 如果函数返回时,出了函数作用域,如果返回对象还未还给系统(全局变量、静态区),则可以使用引用返回,如果已经还给系统了,则必须使用传值返回,不能使用传引用返回.
//常引用
int main()
{
//权限放大: a是只读的,而b是a的引用,竟然是可读可写的,那是不可以的
const int a = 10;
int& b = a;
//权限不变: c和d都是只读的 ,可以
const int c = 10;
const int& d = c;
//权限缩小:f是只读的,可以
int e = 10;
const int& f = e;
}
第一个图解析:d是double类型,产生临时变量,通过隐式类型转换,变为Int类型,第二个是d相当于是不可修改+int 类型,所以要加const
第二个图解析:x1 + x2 这个结果会产生一个临时变量,临时变量是具有常属性的,不可修改,所以 所以这个临时变量可以看作是一个int类型的数据+不可修改这个特性,所以ret 前面要加const
1. 引用 在定义时 必须初始化 ,指针没有要求2. 引用 在初始化时引用一个实体后,就 不能再引用其他实体 ,而指针可以在任何时候指向任何一个同类型实体3. 没有 NULL 引用 ,但有 NULL 指针4. 在 sizeof 中含义不同 : 引用 结果为 引用类型的大小 ,但 指针 始终是 地址空间所占字节个数 (32 位平台下占4 个字节 )5. 引用自加即引用的实体增加 1 ,指针自加即指针向后偏移一个类型的大小6. 有多级指针,但是没有多级引用7. 访问实体方式不同, 指针需要显式解引用,引用编译器自己处理8. 引用比指针使用起来相对更安全9.引用概念上定义一个变量的别名(没开辟空间),指针存储一个变量的地址10.底层汇编实现时:引用是用指针实现的
有时候在 C++ 工程中可能需要 将某些函数按照 C 的风格来编译 , 在函数前加 extern "C",意思是告诉编译器, 将该函数按照 C 语言规则来编译。
1.包含头文件
2.在工程属性中配置静态库的目录和添加静态库注意:可以理解为,只有c++认识extern C
解析:只有C++认识extern "C",静态库为C++文件时,告诉机器按C的修饰规则去找,而C项目中,头文件展开,直接就是函数名
总结:
C++程序调用C的库,在C++程序中加extern"C"
C程序调用C++的库,在C++库中加extern"C"
因为调用函数,需要建立栈帧,栈帧中要保存一些寄存器,结束后又要恢复,频繁的调用函数,这样的操作是有消耗的。而C和C++都有应对的措施。
#define Add(x,y) ((x)+(y))
//最好把((x)+(y))中x和y括号都加上,否则比如(x+y),10*Add(3,4),会替换为10 * 3+4,会导致出错
int main()
{
cout << Add(1, 2) << endl;
return 0;
}
C++ 以 inline 修饰 的函数叫做内联函数, 编译时 C++ 编译器会在 调用内联函数的地方展开 ,没有函数压栈的开销, 内联函数提升程序运行的效率。
查看方式:
1. 在 release 模式下,查看编译器生成的汇编代码中是否存在 call Add2. 在 debug 模式下,需要对编译器进行设置,否则不会展开 ( 因为 debug 模式下,编译器默认不会对代码进行优化 )
1. inline 是一种 以空间换时间 的做法,省去调用函数额开销。 所以代码很长或者有循环/递归的函数不适宜使用作为内联函数。2. inline对于编译器而言只是一个建议 ,编译器会自动优化,如果定义为 inline 的函数体内有循环 / 递归等 等,编译器优化时会忽略掉内联。3. inline 不建议声明和定义分离(直接再定义前面加inline) ,分离会导致链接错误。因为 inline 被展开,就没有函数地址了,因为内联函数都在调用的地方展开了,链接就会找不到。
在早期 C/C++ 中 auto 的含义是:使用 auto 修饰的变量,是具有自动存储器的局部变量 ,但遗憾的是一直没有人去使用它,大家可思考下为什么?C++11 中,标准委员会赋予了 auto 全新的含义即: auto 不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器, auto 声明的变量必须由编译器在编译时期推导而得 。
int main()
{
const int a = 0;
int b = 0;
//自动将a的类型推到c,
auto f = a;//int
auto c = &a;//int const*
auto d = 'A';//char
auto e = 10.11;//double
//f,d,e的类型是const的,具有常性,但是auto会忽略这个const
//typeid打印变量的类型
cout << typeid(f).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
cout << typeid(e).name() << endl;
auto e;//err,auto定义变量时必须对其进行初始化
}
[注意]使用 auto 定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导 auto 的实际类型 。因此 auto 并非是一种 “ 类型 ” 的声明,而是一个类型声明时的 “ 占位符 ” ,编译器在编译期会将 auto 替换为 变量实际的类型 。
用 auto 声明指针类型时,用 auto 和 auto* 没有任何区别,但用 auto声明引用类型时则必须加&
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量 。
void TestAuto()
{
auto a = 1, b = 2;
auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}
在C/C++中遍历数组可以有如下的方式:
//语法糖
int array[] = {1,2,3,4,5};
//c/c++遍历数组
for (int i = 0; i < sizeof(array)/sizeof(int);i++)
{
cout << array[i] << endl;
}
对于一个 有范围的集合 而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此 C++11中引入了基于范围的for循环 。 for 循环后的括号由冒号 “ : ” 分为两部分:第一部分是范围内用于迭代的变量, 第二部分则表示被迭代的范围 。
//C++11 范围for
//自动依次取数组array中的每个元素赋值给e
for (auto e:array)
{
cout << e << endl;
}
注意:与普通循环类似,可以用 continue 来结束本次循环,也可以用 break 来跳出整个循环 。
那如何运用范围for将数组中的每个值+1呢?
for (auto e : array)
{
e++;//此时是array数组将每个值赋值给e,e的改变并不会影响到array数组中的值
}
这个方法是不行的,因为此时是array数组将每个值赋值给e,e的改变并不会影响到array数组中的值,那怎样才能改变呢?可以用引用的方法,数组中的每个值赋值给e变量,而e变量又是该值的别名,修改的是同一份空间,所以会改变数组的值,而应该注意的是每次的e的赋值的作用域只在一个循环之中。这种方法是可以解决这个问题的,代码如下:
for (auto& e : array)
{
e++;
}
for 循环迭代的范围必须是确定的 :对于数组而言,就是数组中第一个元素和最后一个元素的范围 ;对于类而言,应该提供 begin 和 end 的方法, begin 和 end 就是 for 循环迭代的范围。
void TestFor(int array[])
{
for (auto& e : array)
cout << e << endl;
}
int main()
{
int array[] = { 1,2,3,4,5 };
TestFor(array);
}
以上的代码是有问题的,for的范围是不确定的,因为范围for的范围必须是数组名,而数组传参,传过去之后会退化为指针,所以其实TestFor的array是一个指针,所以是有问题的,应当注意
NULL其实是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
此时NULL可能被定义为常量,或者被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦。问题如下:
void f(int)
{
cout << "f(int)" << endl;
}
void f(int*)
{
cout << "f(int*)" << endl;
}
int main()
{
f(0);
f(NULL);
f((int*)NULL);
return 0;
}
没错,运行出来就是如下结果:
解析:
f(NULL)和f(0)对应的都是f(int),而程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的初衷相悖。而在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0, 所以在C++11中,引入了新关键字nullptr。
注意:
1. 在使用 nullptr 表示指针空值时,不需要包含头文件,因为 nullptr 是 C++11 作为新关键字引入的 。2. 在 C++11 中, sizeof(nullptr) 与 sizeof((void*)0) 所占的字节数相同。3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用 nullptr 。