思维导图
目录
命名空间
命名冲突
如何定义命名空间
命名空间定义语法
嵌套定义
同一工程下定义同名命名空间
命名空间的使用
命名空间名称和作用域限定符(: :)
using将命名空间中某个成员“释放”出来
using namespace 命名空间名称
C++标准库命名空间std
C++输入/输出
Hello World的四种写法
自动识别类型
缺省参数
全缺省
半缺省(部分缺省)
缺省值同时出现在声明和定义中
函数重载
构成函数重载的三种场景
参数类型不同
参数个数不同
参数类型顺序不同
缺省和函数重载二义性问题
C++函数修饰规则(linux下测试)
返回值不同不构成函数重载
引用
引用概念
语法特性
应用场景
引用做参数
引用做返回值
常引用
权限的平移(可以)
权限的缩小(可以)
权限的放大(不可以)
临时变量具有常性
引用和指针的对比
内联函数
概念
内联函数的特性
和宏对比
命名空间
在C/C++中,经常要自定义一些变量、函数在全局或者局部,这些变量名和函数名可能会存在冲突的问题。在C++中提出了命名空间的概念,其作用就是对标识符的名称(变量名、函数名等命名)进行本地化,避免和库中的命名或者其他命名空间中的命名冲突。
命名冲突
下述示例代码中定义了一个malloc全局变量,但是这个命名在库中是个函数的名称,出现了命名冲突问题:
#include <stdio.h>
#include <stdlib.h>
//变量名和库中定义冲突
int malloc = 100;
int main()
{
printf("%d\n",malloc);
return 0;
}
上述命名冲突的问题在C++中可以通过定义命名空间的方式来解决!
namespace zxy
{
int malloc = 100;
}
int main()
{
printf("%d",zxy::malloc);
return 0;
}
如何定义命名空间
命名空间定义语法
namespace关键字+命名空间的名称+{}就定义好了一个命名空间:
语法:namespace 命名空间名称 {}
下面的代码中定义了两个命名空间zxy1和zxy2, 两个空间中分别定义了a、b两个变量和add函数,但是它们在不同的域中,不存在冲突问题。
namespace zxy1
{
int a = 985;
int b = 211;
int add(int a, int b)
{
return a + b;
}
}
namespace zxy2
{
int a = 100;
int b = 200;
int add(int a, int b)
{
return (a + b) * 10;
}
}
嵌套定义
在命名空间zxy1中嵌套了一个命名空间zxy2:
namespace zxy1
{
int a = 10;
namespace zxy2
{
int b = 10;
}
}
同一工程下定义同名命名空间
需要注意的是,当在同一工程下定义了同名的命名空间后,不管定义了多少,编译器最后会把它们合成一个命名空间,虽然你定义了多次,但都是在一个作用域中。这时就要注意命名冲突的问题了。
test1.h
namespace zxy
{
int a = 10;
int b = 20;
}
test2.h
namespace zxy
{
int c = 13;
int d = 14;
}
在编译器的视角下,它们是这样的:
namespace zxy
{
int a = 10;
int b = 20;
int c = 13;
int d = 14;
}
1.一个命名空间就定义了一个新的作用域,该空间中的所有内容都局限于该命名空间中。
2.同一工程下同名命名空间最终会被合并成一个。
3.嵌套定义的命名空间,如果想要访问内部的命名空间要先找到其外部的命名空间。
4.关键字namespace 定义命名空间。
命名空间的使用
命名空间名称和作用域限定符(: :)
namespace zxy
{
int a = 10;
int b = 20;
int c = 30;
int add(int x, int y)
{
return a + b;
}
}
int main()
{
printf("%d\n",zxy::a);
printf("%d\n", zxy::add(zxy::b, zxy::c));
return 0;
}
using将命名空间中某个成员“释放”出来
将某个成员从指定命名空间中引入:
namespace zxy
{
int a = 10;
int b = 20;
}
using zxy::a;
int main()
{
printf("%d\n",a);
return 0;
}
using namespace 命名空间名称
该命名空间中的全部成员被引入:
namespace zxy
{
int a = 10;
int b = 20;
int c = 30;
int add(int x, int y)
{
return a + b;
}
}
using namespace zxy;
int main()
{
printf("%d\n",a);
printf("%d\n",add(c,b));
return 0;
}
1.(: :)作用域限定符。
2.using + 命名空间名称 + 作用域限定符 + 命名空间中某个成员(using N: :a),将命名空间中的某个成员引入。
3.using+namespace+命名空间名(using namespace N),将整个命名空间中的内容引入。
4.命名空间名称+作用域限定符+某个成员(N : : x)。
C++标准库命名空间std
初始C++的过程中,常见的一行代码:
using namespace std;
基于上述命名空间的知识,这行代码的作用是将C++标准库命名空间的内容展开,这样做的目的是在日常练习中更加的便捷的使用std空间中的内容。缺点是文章开头提到的命名冲突问题。
C++输入/输出
1.cout是ostream类型的对象,cin是istream类型的对象,endl表示换行,它们都包含在<iostream>头文件中。
2.<<是流插入运算符,>>是流提取运算符。
3.C++标准库的定义实现都放到了std命名空间中。
Hello World的四种写法
有了上述的描述,现在动手写一个“Hello World”:
#include <iostream>
#include <stdio.h>
//方法2:全部引入
using namespace std;
//方法3:部分引入
using std::cout;
using std::cin;
int main()
{
//方法1:不引入
std::cout << "Hello World!" << std::endl;
//方法2和3写法一样:
cout << "Hello World!" << endl;
//方法4:printf(C++兼容C)
printf("Hello Word!\n");
return 0;
}
上述代码展示了命名空间的几种不同用法,也使用了cout标准输出对象。但是有些场景下printf也有它的一些优势,根据实际情况选择不同的实现方法。
自动识别类型
#include <iostream>
using namespace std;
int main()
{
int a;
double b;
char ch;
cin >> a >> b >> ch;
cout << "int:" << a << endl;
cout << "double:" << b << endl;
cout<<"char:" << ch << endl;
return 0;
}
自动识别类型的原理
ostream和istream类中分别对<<流插入和>>流提取运算符进行了重载:
缺省参数
全缺省
#include <iostream>
using namespace std;
void Fun(int a = 10, int b = 20)
{
cout << "a=" << a << ",b=" << b<<endl;
}
int main()
{
Fun();//不传参,用缺省值
Fun(100, 200);//传参
return 0;
}
半缺省(部分缺省)
半缺省给缺省值的时候必须从右往左依次给,不能出现间隔。
错误示例:
void Fun(int a = 10, int b = 30,int c)
{
cout << "a=" << a << ",b=" << b<<endl;
}
void Fun(int a = 10, int b,int c = 20)
{
cout << "a=" << a << ",b=" << b<<endl;
}
正确使用
#include <iostream>
using namespace std;
void Fun(int a, int b = 10,int c = 20)
{
cout << "a=" << a << ",b=" << b<<endl;
}
int main()
{
Fun(10);
Fun(100, 200);
Fun(100, 200,300);
return 0;
}
在传参的时候是按照顺序穿的,不可以间隔这传参!如:Fun(100, , 200);这是错误的写法。
缺省值同时出现在声明和定义中
在声明和定义中同时出现缺省值是不可以的,如果两个位置给的缺省值不同,编译器就无法确定该用哪个缺省值,所以不支持这样的语法。解决方法就是只在声明中给缺省值。
#include <iostream>
using namespace std;
void Fun(int a = 30, int b = 10, int c = 20);
int main()
{
Fun(10);
Fun(100, 200);
Fun(100, 200,300);
return 0;
}
void Fun(int a, int b, int c)
{
cout << "a=" << a << ",b=" << b << endl;
}
1.部分缺省的缺省值要从右向左给,不能间隔给缺省值。
2.部分缺省传参的顺序是从左向右传参,不能间隔传参。
3.缺省值不能在声明和定义中同时出现,只在声明中给。
函数重载
C++中允许在同一作用域中出现同名函数,这些同名函数的形参列表必须不同(参数个数不同,参数类型不同,参数顺序不同)。
构成函数重载的三种场景
参数类型不同
//类型不同是不同
int add(int a, int b)
{
cout << "int:" ;
return a + b;
}
double add(double c, double d)
{
cout << "double:" ;
return c + d;
}
参数个数不同
//个数不同是不同
void fun(int a)
{
cout << "一个参数"<< endl;
}
void fun(int c, int d)
{
cout << "两个参数" <<endl;
}
参数类型顺序不同
//参数类型顺序不同
void fun2(int a, char b)
{
cout << "int ,char" << endl;
}
void fun2(char c, int d)
{
cout << "char int" << endl;
}
缺省和函数重载二义性问题
下述代码中,无参的fun和两个参数的fun构成了函数重载,但是有参的fun参数给了缺省值,调用的时候以可以fun();调用,当在main函数中调用fun()时,就出现了函数调用不明确的错误。
int fun()
{
cout << "fun()" << endl;
}
int fun(int a = 10,int b = 20)
{
cout << "fun(int,int)" << endl;
}
int main()
{
//二义性,调用不明确
fun();
return 0;
}
C++函数修饰规则(linux下测试)
C++支持函数重载是因为C++通过函数修饰规则将同名函数进行了修饰,对于上述参数不同的情况修饰出来的名字不同!
●原理分析
一个程序要运行,需要经历预处理、编译、汇编、链接几个阶段!在链接阶段,当链接器看到有函数调用时,但没有该函数的地址,会去寻找该函数的地址,然后链接到一起。C语言中同名函数没法区分,所以不支持重载。C++对函数名进行了修饰(编译器不同修饰规则可能不同),在链接器的视角来看就是不同的函数名称,所以C++支持函数重载。
●Linux下g++测试
#include <iostream>
using namespace std;
int fun(int a,int b)
{
return a+b;
}
int fun(int c,double d)
{
return c;
}
int main()
{
fun(10,20);
fun(15,23.4);
return 0;
}
objdump -S xxxxxx
返回值不同不构成函数重载
按照上述函数名修饰规则支持函数重载的原理,将返回值类型也进行修饰,就可以对同名函数进行区分。但是语法上不支持这样做,原因是在函数调用阶段,没办法指明返回值,函数调用存在二义性。所以返回值不同,不能构成函数重载。
int add(int a, int b)
{
return 10;
}
double add(int c, int d)
{
return 13.14;
}
int main()
{
//函数调用阶段无法指明返回值
add(1,2);
add(3, 4);
}
1.构成函数重载的三种条件,参数个数不同,参数类型不同,参数类型顺序不同。
2.全缺省和无参同名函数在调用时可能存在调用不明确的问题。
3.返回值不同不能构成函数重载,原因是在函数调用阶段无法指明返回值。
4.C++支持函数重载的原因,是根据函数名修饰规则对同名函数进行修饰,当满足函数重载条件(“三不同”)时,在链接器看来它们就是不同的函数名。
引用
C++中的引用和指针是相互辅佐的关系,有一部分功能是重叠的,各自又有其自身的特点。C++中的引用不能完全的替代指针。
引用概念
引用不是新定义一个变量,而是给已经存在的变量取一个别名,编译器不会为引用变量开辟空间,它和被引用的变量公用一块内存空间。
●ra引用变量a,在语法层面上没有给引用变量开辟空间。ra和a共用同一块内存空间。
int main()
{
int a = 100;
int& ra = a;
cout <<"&a:" << &a << endl;
cout <<"&ra:" << &ra << endl;
return 0;
}
取地址a和取地址ra得到的结果是相同的,说明两者共用同一块空间。
int main()
{
int a = 100;
int* ra = &a;
cout <<"&a:" << &a << endl;
cout <<"&ra:" << &ra << endl;//取到的是指针的地址
return 0;
}
取地址ra取的是指针变量的地址,如果想要取到a的地址,要先对指针变量解引用在取a的地址。
语法特性
引用在定义时必须初始化!
int a = 10;
int& ra = a;
const int& rb = 20;
一个变量可以有多个引用。
int a = 10;
int& ra = a;
int& rra = a;
int& rrra = rra;
引用一旦引用一个实体,就不能在引用其他实体。
应用场景
引用做参数
传参局部解析(传值调用)
观察上述局部的汇编代码解析图,在add函数调用的过程中,首先先将实参拷贝到寄存中,在压入到函数栈帧。“形参是实参的一份临时拷贝”,通过上图可以更好的理解这句话。
●引用做参数(输出型参数)的优点是减少了一次拷贝,修改形参,实参也跟着修改。这部分功能和指针是重叠的。
void Swap(int& left, int& right)
{
int tmp = left;
left = right;
right = tmp;
}
int main()
{
int a = 10,b=20;
cout << "交换前:a=" << a << ",b=" << b << endl;
Swap(a,b);
cout << "交换后:a=" << a << ",b=" << b << endl;
return 0;
}
引用做返回值
●栈帧销毁,返回值返回过程
如上图所示,在函数返回值返回的过程中,并不是直接返回,而是生成临变量拷贝到寄存器上,如果数据较大也可能保存在上层栈帧中,在间接返回给ret。
●出了作用域返回对象还在(传引用返回,减少拷贝)
int& Fun2()
{
static int N = 10;
N++;
return N;
}
int main()
{
int ret2 = Fun2();
cout << ret2 << endl;
return 0;
}
●出了作用域返回对象不在(传值返回)
int Fun1()
{
int N = 10;
N++;
return N;
}
int main()
{
int ret1 = Fun1();
cout << ret1 << endl;
return 0;
}
当函数返回时,出了函数作用域,如果要返回的对象还在,适用于传引用返回,优点是减少了一次拷贝。如果出了函数作用域对象销毁,则要使用传值返回。
●错误场景
出了函数作用域销毁的局部变量,采用传引用返回:
int& Fun3()
{
int a = 100;
return a;
}
int main()
{
int ra = Fun3();
return 0;
}
需要注意的是,当函数调用结束,栈帧销毁后。并不是空间不存在了,而是失去了访问该空间的权利,该空间也不在被保护。
常引用
在指针和引用的赋值中,存在权限的放大和缩小问题!!!
权限的平移(可以)
int main()
{
int a = 10;
const int b = 20;
int& ra = a;//权限的平移
const int& rb = b;//权限的平移
return 0;
}
权限的缩小(可以)
int main()
{
int a = 10;
const int& rra = a;//权限的缩小
return 0;
}
权限的放大(不可以)
int main()
{
const int b = 20;
int& rrb = b;
return 0;
}
临时变量具有常性
如上述的场景中,在类型转换或者传值返回的过程中,会产生临时变量,它们是具有常性的,对它们引用,要用常引用。
场景1:
int main()
{
int a = 10;
double b = 13;
const int& rb = b;
return 0;
}
场景2:
int Fun()
{
int n = 10;
return n;
}
int main()
{
const int& rn = Fun();
return 0;
}
引用和指针的对比
1.引用在底层上实际上是有空间的,引用是按照指针方式实现的。在语法层认为,引用是一个别名,没有独立空间,和引用实体共用同一块空间。
2.引用和指针是互补的,各有优点。
3.引用在概念上是一个变量的别名不需要额外的空间(语法层面上),指针要存储一个变量的地址。
4.引用在定义时必须初始化,指针没有要求。
5.引用很专一,引用了一个实体后就不能在引用其它实体。指针的指向随意改变。这也是引用无法代替实体的原因只一,不能改变指向,诸如链表的增删查改就无法得到很好的实现。
6.没有空引用,但有空指针。
7.在sizeof中的含义不同,sizeof(引用)计算的是引用实体的类型大小,指针不管指向的是什么类型的数据,计算的都是地址空间所占字节的大小,32位平台占4字节,64位平台下占8字节。
8.引用++和--是引用的实体改变,指针++和--是向后或者向前偏移一个类型的大小。
9.有多级指针,没有多级引用,引用一个别名,实际上和引用实体是一样的。
10.访问实体的方式不同,指针需要解引用,引用编译器自己处理,用户直接使用即可。
11.引用比指针更加的安全。
内联函数
概念
以关键字inline修饰的函数叫做内联函数,编译器会在编译的预处理阶段将内联函数展开,没有函数调用建立栈帧的消耗,内联函数提升程序运行的效率。
●正常函数调用
int add(int x, int y)
{
return x + y;
}
int main()
{
int a = 10, b = 20;
int ret = add(a, b);
cout << ret << endl;
return 0;
}
上述汇编代码是正常函数调用,call add跳转到目标函数。
●内联函数调用
在debug模式下,编译器默认不会展开inline函数,需要对编译器进行设置。
inline int add(int x, int y)
{
return x + y;
}
int main()
{
int a = 10, b = 20;
int ret = add(a, b);
cout << ret << endl;
return 0;
}
通过对汇编代码的观察可以发现,内联函数确实被展开了。
内联函数的特性
1.inline是以空间换时间的做法,如果将编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用。缺点:最后形成的目标文件会变大。优点:少了函数调用创建栈帧的开销,提高程序的运行效率。
2.函数前加了inline关键字,只是对编译器的建议,编译器可以忽略。一些比较成的函数编译器不会将其展开。
inline int add(int a, int b)
{
int c = a + b;
c = a + b;
c = a + b;
c = a + b;
c = a + b;
c = a + b;
c = a + b;
c = a + b;
c = a + b;
c = a + b;
c = a + b;
c = a + b;
c = a + b;
c = a + b;
c = a + b;
c = a + b;
c = a + b;
c = a + b;
return c;
}
int main()
{
int a = 10, b = 20;
int sum = 0;
sum = add(a,b);
return 0;
}
如上述测试函数,进行了inline声明。但是最终是否展开取决于编译器,较长的函数,编译器会忽略内联的请求。
3.内联适用于代码规模小,频繁调用的函数。
4.内联函数的声明和定义分离会导致报错,原因是内联函数函数名不进入符号表,在链接的过程中,找不到函数地址。
add.cpp
inline int add(int a, int b)
{
int c = a + b;
return c;
}
add.h
#pragma once
int add(int a, int b);
test.cpp
#include <iostream>
using namespace std;
#include "add.h"
int main()
{
int a = 10, b = 20;
int sum = 0;
sum = add(a,b);
cout << sum << endl;
return 0;
}
链接错误:
和宏对比
●替代宏的技术
1.常量的定义:const或者枚举enum
2.短小函数的定义,用内联函数
●宏和内联函数对比
1.代码长度
宏在预处理阶段进行替换,内联函数在编译阶段展开。都会使程序的长度变长。
2.执行速度
宏和内联函数都没有函数栈帧创建和销毁的消耗,执行速度比函数调用快。
3.参数类型
宏的参数与类型无关,不进行类型检测,所以宏的使用不够严谨。内联函数的使用和常规函数相同,参数与类型是有关的。
4.调试
宏不支持调试,内联函数支持调试。