入门C++
- 命名空间
- 命名空间定义
- 命名空间的使用
- C++输入,输出
- 缺省参数
- 缺省参数概念
- 缺省参数分类
- 函数重载
- 函数重载概念
- C++支持函数重载的原因
- 引用
- 引用概念
- 引用特性
- 具体应用
- const修饰的常量进行引用(常引用)
- 传值,传引用的效率
- 引用和指针的区别
- 内敛函数
- 概念
- 特性
- auto关键字
- 类型别名思考
- auto简介
- auto的使用规则
- auto不能推导的场景
- 空指针
命名空间
#include<stdio.h>
#include<stdlib.h>
int rand = 1;
int main()
{
printf("%d\n", rand);
return 0;
}
在C语言的编译环境下,头文件 #include<stdlib.h>
展开之后会包含rand()
函数,编译默认的查找成员方式是:先局部,后全局。所以当函数rand()
和变量rand
同时出现在全局时,程序便会报错
此类情况是不可避免的,尤其是在工程中,命名冲突在所难免,不容小觑。
C++编译系统,为此提供一个解决办法,命名空间
对标识符名称进行本地化,避免命名冲突(名字冲突)
命名空间就类似于在成员与外界之间设置一个围墙,如果想要访问其中的成员,要么通过专属的通道(作用域限定符),要么将围墙进行拆除。
将变量rand
放在命名空间namespace M
中,按照查找规则,在全局中找到rand()
函数;通过域作用限定符,在命名空间中找到变量rand
,由此避免了命名冲突
#include<stdio.h>
#include<stdlib.h>
namespace M
{
int rand = 1;
}
int main()
{
printf("%p\n", rand);
//:: 域作用限定符
printf("%d\n", M::rand);
return 0;
}
命名空间定义
命名空间的定义,需要使用关键字namespace
,实例如下
namespace name //name是命名空间的名称
{
member //命名空间的成员
}
举例如下
namespace M
{
int m = 'm';
int n = 'n';
}
与C语言中的结构体类似,命名空间同样也可以嵌套定义
namespace M
{
member;
namespace N
{
member;
}
}
举例如下
namespace M
{
int m = 'm';
int n = 'n';
namespace N
{
int i = 'i';
int j = 'j';
}
}
命名空间定义了新的作用域,命名空间中所有的成员生命周期仅局限于命名空间
命名空间的使用
- 加命名空间名称及作用域限定符
int main()
{
printf("%d", M::m);
return 0;
}
- 使用using将命名空间中某个成员引入
using
作用就类似于打开围墙的专属通道,可以直接访问成员m,但对于成员n的访问还是需要使用作用域限定符
using M::m;
int main()
{
printf("%d\n", m);
printf("%d\n", M::n);
return 0;
}
- 使用using namespace 命名空间名称引入
using namespace
的作用就类似于,直接将围墙进行拆除,可以直接访问所有成员
using namespace M;
int main()
{
printf("%d\n", m);
printf("%d\n", n);
return 0;
}
C++输入,输出
std
是C++标准库的命名空间名,C++将标准库的定义实现全部都放在其中
#include<iostream>
using namespace std;
int main()
{
cout << "hello crush" << endl;
return 0;
}
- 使用cout标准输出对象和cin标准输入对象时,需要包含
<iostream>
头文件以及按命名空间使用的方式使用std
- cout和cin时全局的流对象,endl时特殊的C++符号,表示换行输出,类似于C语言中的
'\n'
,全部都包含在<iostream>
头文件中 - <<是流插入运算符,>>是流提取运算符
- C++的输入输出可以自动识别变量类型
举例如下
#include<iostream>
using namespace std;
int main()
{
char c = 'c';
int i;
//输入变量i的数值
cin >> i;
//打印输出变量 i,c
cout << i << endl;
cout << c << endl;
return 0;
}
缺省参数
缺省参数概念
缺省参数是声明(定义)函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参
#include<iostream>
using namespace std;
void test(int i = 1)
{
cout << i << endl;
}
int main()
{
test(0);
test();
return 0;
}
缺省参数分类
全缺省参数
#include<iostream>
using namespace std;
void test(int a = 1, int b = 2, int c = 3)
{
cout << a << " " << b << " " << c << endl;
}
int main()
{
test();
return 0;
}
半缺省参数(部分缺省)
只能从右往左连续缺省
#include<iostream>
using namespace std;
void test(int a , int b = 2, int c = 3)
{
cout << a << " " << b << " " << c << endl;
}
注意
- 半缺省参数必须传递实参,且顺序从左往右,不能间隔赋值
- 缺省参数不能在函数声明和定义中同时出现,规定只能在声明中存在。因为如果声明和定义中同时存在缺省参数,到底以哪一个作为真正的缺省参数呢?
头文件中声明,源文件中定义。由于同时都存在缺省参数,所以程序会报错
- 缺省值必须是常量或者全局变量
函数重载
词语具有一词多义的特点,只有通过上下文才能判断该词语的真正的含义,函数重载也是具有类似的特点
函数重载概念
函数重载:函数的一种特殊情况,允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数,类型,类型顺序)不同,常用来处理实现功能类似数据类型不同的问题
形参类型不同
int Add(int left, int right)
{
cout << "int Add(int left, int right)" << endl;
return left + right;
}
double Add(double left, double right)
{
cout << "double Add(double left, double right)" << endl;
return left + right;
}
形参个数不同
void test()
{
cout << "test()" << endl;
}
void test(int i)
{
cout << i << endl;
}
参数类型顺序不同
void test(int i, char c)
{
cout << "void test(int i, char c)" << endl;
}
void test(char c, int i)
{
cout << "void test(char c, int i)" << endl;
}
还存在一个小问题 函数重载遇上缺省参数
//构成函数重载,同时也是缺省参数
//调用存在歧义,到底该调用哪一个不清楚
void test(int a = 0, int b = 1)
{
cout << "test(int a,int b)" << endl;
}
void test()
{
cout << "test()" << endl;
}
int main()
{
test();//二义性
return 0;
}
C++支持函数重载的原因
C语言编译结果(简化)
函数名不同,在符号表中才能区分出来,所以不能支持重载
C++编译结果(简化)
C++是通过函数修饰规则在符号表中区分的,只要参数不同,修饰的出的函数名就不同,所以支持重载
重载的概念一直都没有提到返回值,也就是说返回值并不会构成重载原因,但是为什么呢???
int test(int a, int b)
{
cout << "test(int a,int b)" << endl;
return a + b;
}
char test(int a, int b)
{
cout << "test(int a,int b)" << endl;
return a + b;
}
int main()
{
test(0,0);
test(1, 1);
return 0;
}
在函数调用时存在二义性,没有指定返回值类型,无法进行区分,并不是函数名修饰规则的原因,所以不构成重载
引用
引用概念
引用是给已经存在的变量另取一个名字或者说小名,但是不会为引用变量新开辟内存空间,所以变量和引用变量共用同一块内存空间
变量类型&引用变量名称=引用实体
#include<iostream>
using namespace std;
int main()
{
int i = 1;
int& ri = i;
printf("%p\n", &i);
printf("%p\n", &ri);
return 0;
}
既然是对已存在的变量进行引用,当然数据类型要求是一致的
引用特性
- 引用在定义时必须要初始化
- 一个变量可以有多个引用(小名)
- 若引用(小名)已经引用一个变量,则不能再引用其他变量;即一个小名不能同时称呼两个人
具体应用
形式参数
#include<iostream>
using namespace std;
void Swap(int& ra, int& rb)
{
int tmp = ra;
ra = rb;
rb = tmp;
}
int main()
{
int a = 0;
int b = 1;
printf("%d %d\n", a, b);
printf("%p %p\n", &a, &b);
Swap(a, b);
printf("%d %d\n", a, b);
printf("%p %p\n", &a, &b);
return 0;
}
优点
- 减少拷贝,提高效率
- 输出型参数,在函数中修改形参,实参也随之改变
返回值
- 引用返回
int& Test()
{
static int i = 0;
i++;
return i;
}
int main()
{
int ret = Test();
cout << ret << endl;
return 0;
}
函数栈帧的使用是从上往下(高地址到低地址),当 函数int& Test()
结束时,栈帧被销毁。变量i保存在静态区,所以并不会被销毁,然后返回i的别名给 ret
如果变量i不在静态区
int& Test()
{
int i = 0;
cout << &i << endl;
i++;
return i;
}
void test()
{
int a = 0;
cout << &a << endl;
}
int main()
{
int& ret = Test();
cout << ret << endl;
cout << ret << endl;
test();
cout << ret << endl;
cout << &ret << endl;
return 0;
}
int& ret = Test()->int&ret = int& i
所以 ret 也就是 i 本身
由于变量i是 int& Test()
内的局部变量,所以当函数栈帧销毁时,i的空间也被收回。可以继续访问,但是数据是不确定的,而且 使用cout
也会调用函数的
当函数 test()
开辟栈帧时,碰巧使用前面函数 Test()
函数栈帧的空间,也就是说,当访问 ret 时,其实就是访问变量 a 本身
运行结果也表面,两个函数 Test()
, test()
,开辟栈帧使用的确实是同一块空间
第一次打印 ret 时,数据没有改变,可能是 ret 是作为参数传递给 cout
,所以数值没有改变
第二次打印 ret 时,数据是随机值,也就是 cout
调用函数将被收回 i 的空间数据进行了改变
- 传值返回
int Test()
{
static int i = 0;
i++;
return i;
}
int main()
{
int ret = Test();
cout << ret << endl;
return 0;
}
与引用返回类似,变量 i 存放在静态区,栈帧销毁不会收到影响,反而作为临时变量返回给 ret
如果变量i不在静态区
int Test()
{
int i = 0;
i++;
return i;
}
int main()
{
int ret = Test();
cout << ret << endl;
return 0;
}
由于变量i是 int& Test()
内的局部变量,所以当函数栈帧销毁时,i的空间也被收回。可以继续访问,但是数据是不确定的,而且 使用cout
也会调用函数
总结
出了函数作用域,返回变量不存在,不能使用引用返回,引用返回的结果是未定义的(未知的)
出了函数作用域,返回变量还存在,才能使用引用返回
int Test1()
{
int i = 0;
i++;
return i;
}
int Test2()
{
static int i = 0;
i++;
return 0;
}
优点
- 减少拷贝,提高效率
- 可修改返回值
变量没有被const
修饰时一般是可读可写的,可读可写是权限,在程序中,权限可以缩小,平移,但是不可以扩大
int main()
{
//a可读可写
int a = 0;
//ra是a的别名,同样是可读可写,权限平移
int& ra = a;
//rra被const修饰,只能读,权限缩小
const int& rra = a;
//b被const修饰,只能读
const int b = 0;
//rb是b的别名,可读可写,权限扩大 error
int& rb = b;
//rb被const修饰,只能读,权限平移
const int& rb = b;
return 0;
}
结合上面的传引用做参数,一般都是需要const
修饰,否则程序会报错
void test(int& x)
{}
int main()
{
int a = 0;
const int& rra = a;
test(a);
test(rra);
return 0;
}
const修饰的常量进行引用(常引用)
数据类型转换时会产生临时变量,且临时变量具有常性,不可改变
int main()
{
double a = 1.2;
int b = a;
cout << b << endl;
return 0;
}
输出的结果也不是a本身,而是类型转换后产生的临时变量
再分析下面的代码
int main()
{
double a = 1.2;
int i = a;
int& ri = a;
return 0;
}
传值,传引用的效率
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时拷贝(函数栈帧中有详解),因此用值作为参数或者返回值类型,效率比较低,当参数或者返回值类型非常大时,效率就会更低;相反传引用就更加的高效
引用和指针的区别
语法概念上引用是小名,没有新开辟的空间,与引用变量共用同一块空间
底层实现上引用实际是有空间的,引用时按照指针的方式来实现的
#include<iostream>
using namespace std;
int main()
{
int i = 0;
int& ri = i;
ri = 1;
int* pi = &i;
*pi = 1;
return 0;
}
引用与指针的汇编代码是一致的,所以两者实现方式是一致的
引用与指针的不同点
- 引用概念上定义变量的别名(小名),指针存储变量的地址
- 引用在定义时必须初始化,指针没有要求
- 引用在引用(修饰)一个变量之后,不能再引用(修饰)其他变量;指针可以指向任何同一类型的变量
- 不存在NULL引用;存在NULL指针
- sizeof计算的大小不同:引用的结果是引用(修饰)的变量类型的大小;指针始终是地址空间所占字节大小
内敛函数
概念
由 inline
修饰的函数称作内联函数,程序运行时,编译器会在调用内联函数的位置展开,没有函数调用建立栈帧,内敛函数有利于提升程序运行效率
在debug模式下,需要对编译器进行设置,否则不会展开。因为在debug模式下,需要对函数进行调试
对比如下
int Add(int a, int b)
{
return a + b;
}
int main()
{
int ret = Add(1, 1);
return 0;
}
内联函数
inline int Add(int a, int b)
{
return a + b;
}
int main()
{
int ret = Add(1, 1);
return 0;
}
编译时,内联函数会直接展开,不会开辟空间建立栈帧
特性
- inline是通过空间换时间的方式。在运行时编译器如果将函数当作内敛函数处理,则在编译阶段,会用函数体替换函数调用;缺点:目标文件可能会变大;优点:不需要调用函数栈帧,提高程序运行效率
- inline只是一个建议,不同编译器对于inline的实现方式不同,一般建议:函数规模较小,不是递归,且频繁调用的函数采用inline修饰
- inline不能声明和定义分离,会导致链接错误。只要函数声明被inline修饰,预处理时,函数的信息就不会在符号表中,链接阶段就会找不到从而出现错误
auto关键字
类型别名思考
随着程序越来越复杂,程序中需要使用的类型也会越加复杂,
一般体现在:
- 类型拼写较难
- 含义不明确会导致出错
auto简介
auto作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译期间推导而得
#include<iostream>
using namespace std;
int main()
{
int i = 0;
auto a = i;//自动识别变量a的类型为 int
auto b = 'i';//自动识别变量b的类型为 char
cout << i << endl;
cout << a << endl;
cout << b << endl;
return 0;
}
auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种类型的声明,而是一个类型声明时的占位符,编译器在编译阶段会将auto替换为变量实际的类型
auto的使用规则
auto与指针和引用结合使用
用auto声明指针类型时,auto与auto*没有区别,但是auto声明引用类型时必须加上&
#include<iostream>
using namespace std;
int main()
{
int i = 0;
auto a = &i;//a的类型是 int* 存储变量i的地址
auto* b = &i;//b的类型与a一致,同样存储变量i的地址
auto& c = i;//由于&的存在 c的类型便是int& 就是变量i的引用
cout << a << endl;
cout << b << endl;
cout << c << endl;
return 0;
}
同一行定义多个变量
当在同一行声明多个变量时,这些变量必须时相同的类型,否则编译器便会报错,因为编译器实际上只对第一个类型进行推导,然后用推导(自动识别)的类型定义其他变量
void test()
{
auto a = 1, b = 2;
auto c = 3, d = 'a';
}
auto不能推导的场景
auto不能作为函数的参数
void test(auto i)
{
}
auto不能作为形参类型,因为编译器无法对i的实际类型进行推导(自动识别)
auto不能直接用来声明数组
void test()
{
int a[] = { 1,2,3 };
auto b[] = { 1,2,3 };
}
空指针
- 在使用nullptr表示空指针时,不需要包括头文件
- sizeof(nullptr)与sizeof((void*)0)所占的字节数相同
- 规定之后使用nullptr表示空指针