在C/C++中,变量、函数和类都是大量存在的,而这些变量、函数和类的名称大多数都存在于全局作用域中,这就很容易导致出现相同的名称。而命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字就可以很好的解决这一问题。
如下问题:
#include
#include
int scanf = 10;
int main()
{
printf("%d\n", scanf);
return 0;
}// 编译后后报错:error C2365: “scanf”: 重定义;以前的定义是“函数
C语言没办法解决类似这样的命名冲突问题,所以C++就提出了用namespace来解决。
定义命名空间,需要使用到namespace关键字,后面在写上命名空间的名字,然后接一对{ }即可,{ }中即为命名空间的成员。
说明:
(1)命名空间里面可以定义变量,函数,类。模板,和其他命名空间,即可以出现在全局作用域中的声明就可以置在命名空间中!
(2)命名空间分割了全局作用域,每个命名空间都是不同的作用域,我们可以在命名空间里面定义我们需要的变量,函数,类,模板或其他命名空间。
如下:
namespace ikun
{
int a = 10;//定义变量
int add(int a,int b)//定义函数
{
return a+b;
}
class cxk//定义类
{
public:
int test()
{
return 5;
}
private:
int n;
char ch;
};
}
上面的 “ikun” 是命名空间的名字,命名空间里面我们定义了变量a、函数add,类cxk。当我们定义完命名空间后,就产生了一个新的作用域,即该作用域的名称为ikun,该作用域里有变量a、函数add、类cxk。
使用命名空间里的成员有三种方式:
(1)语法:命名空间的名称 + 作用域限定符 + 成员。 C++中的 : : 就是作用域限定符。如我们需要使用ikun命名空间里的成员时,我们就可以这样访问:
printf("%d\n",ikun::a);//使用命名空间里的变量。--输出10
printf("%d\n",ikun::add(39,4));//使用命名空间里的函数。--输出43
而上面的ikun : : 我们也称为前缀,当我们不想写前缀时,我们就可以使用第二种方式。
(2)语法:using + 命名空间的名称 + 作用域限定符 + 成员。 使用using声明将命名空间中某个成员引入。引入之后在使用该成员时就不用在写前缀。
using ikun::a;// 使用using声明将ikun命名空间里的scanf变量引入
using ikun::add;//使用using声明将ikun命名空间里的函数引入
引入之后我们不加前缀就可以使用该命名空间里的成员,如下:
printf("%d",a);
printf("%d",add(44,5));
(3)语法:using + namespace + 命名空间的名称。 使用using namespace 将命名空间名称引入,引入之后我们在使用命名空间里的成员时,全部都不用加前缀。
using namespace ikun;//将命名空间ikun引入
引入命名空间ikun后,它的全部成员我们使用其时就可以不用加前缀了,如下:
printf("%d\n",a);
printf("%d\n",add(36,5));
注意: 第三种方法虽然方便了很多,但是也充满风险。
(1)只使用一条语句就突然将命名空间中的所有成员变得可见了,如果程序中存在多个不同的库,而这些库都通过using指示使得全部成员变得可见,那么全局命名空间污染的问题将重现。
(2)引发的二义性错误只有在使用了冲突名字的地方才能被发现这种延后的检测意味着可能在特定的库引入很久之后才爆发冲突。直到程序使用到该库的新部分后,之前一直未被检测到的错误才被发现。
(3)相比于使用第三种方式,在程序中使用第二种方式对命名空间中的成员分别使用using声明效果会更好,因为第二种方式可以减少注入到命名空间中的名字数量。以及using声明引起的二义性问题在声明处就能发现,不用等到使用名字的地方,这显然对检测和修改错误大大的提高了效率!
C++并未定义任何输入输出语句,取而代之的是一个更全面的标准库(standard library)来提供IO机制。iostream 库包含了两个基础类型istream和ostream,分别表示输入流和输出流。一个流就是一个字符序列,即从IO读出或写入IO设备的过程。
(1)处理输入时,我们使用一个名为 cin 的 istream类型的对象,该对象也称为标准输入流。
(2)处理输出时,我们使用一个名为 cout 的ostream类型的对象,该对象也称为标准输出流。
(3)cin和cout是全局的流对象,他们都包含在包含 < iostream > 头文件中。cout和cin都是C++标准库里的std命名空间,C++将标准库的定义实现都放到这个命名空间中。
(4)cout和cin自动识别数据的类型,无需像printf/scanf输入输出那样,需要手动控制格式。
缓冲区是内存空间的一部分,也就是说在内存空间中预留了一定大小的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区,根据其对应的是输入设备还是输出设备,分为输入缓冲区和输出缓冲区。
当执行cout语句时,会先把插入的数据按顺序存放在输出缓冲区中,当输出缓冲区满或遇到cout语句中的endl(或’\n’,ends,flush)时,在将缓冲区中的数据一起输出,并清空缓冲区。输出流中的数据在系统默认的设备(一般为显示器)输出。
cout语句的一般格式为:
cout<<表达式1<<表达式2<<表达式3…<<表达式n;
#include
using namespace std;
int main()
{
int a = 10;
cout<<a<<endl;//输出变量a
cout<<a+1<<a+7<<a+5<<endl;//可以输出多个表达式
return 0;
}
说明:
(1)<<是流插入运算符,它把右测的信息插入到左边的cout输出流中,即把数据写到标准输出流对象cout中。
(2)cout默认不换行。而endl是特殊的C++符号,表示结束当前行并将与设备关联的缓冲区中的内容刷新到设备中。
(3)cout输出字符串时可以使用转义字符。
我们知道从设备(键盘)上输入数据时,数据会先在输入缓冲区存放着。而当使用cin时,我们从键盘输入数据时需要敲一下回车键才能够将数据送入到输入缓冲区中,其中回车键(\r)会被转换为\n换行符,该换行符\n也会被存储在cin的输入缓冲区中并且被当成一个字符来计算!例如我们在键盘上输入abc这些字符串,再敲回车键,那么此时输入缓冲区中的字节个数是4 ,而不是3。
cin语句的一般格式为:
cin>>变量1>>变量2>>变量3>>…>>变量n;
#include
using namespace std;
int main()
{
int n;
char c;
cin>>n;
return 0;
}
说明:
(1)>>是流提取运算符,即将输入流里的数据提取出来赋值给内存中的变量。
(2)cin输入时,系统会根据变量的类型从输入流中提取相应长度的字节。
(3)把空格字符和回车换行符作为字符输入给字符变量时,它们将被跳过。
(4)cin可以连续从键盘读取想要的数据,以空格、enter或换行作为分隔符。
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。当在调用该函数时,如果没有指定实参,则采用该形参的缺省值,否则使用指定的实参。
全缺省参数,就是给函数的全部参数都指定缺省值。
void func1(int a = 3, int b = 9, int c = 7)//指定缺省值
{
cout <<"a = "<< a << endl;//输出3
cout <<"b = "<< b << endl;//输出9
cout <<"c = "<< c << endl;//输出7
}
int main()
{
func1();//未传入实参
return 0;
}
在上面的例子中,我们在定义func函数时,给函数的参数指定了三个缺省值,而当我们在调用该函数时并没有给该函数指定实参,此时该函数的参数就采用我们刚刚所给的缺省值。
半缺省参数其实也就是在函数的全部的参数中只指定一些参数的缺省值。
void func2(int x, int y = 6 , int z = 71)//指定两个缺省值
{
cout << "x = " << x << endl;
cout << "y = " << y << endl;
cout << "z = " << z << endl;
}
int main()
{
func2(34);
return 0;
}
因为实参传给形参的时候是从左边开始依次传入的,所以半缺省参数必须 “ 从右往左依次 ” 来指定,不能间隔着指定。
错误示例:
例1:
void test1(int a = 2, int b, int c = 7)//error,间隔着指定缺省值
{
}
int main()
{
test1(8);//error,
return 0;
}
实参的8从左边的参数开始传入,但a有缺省值了,如果传给a,那b就没有参数。如果传给b,那就不符合我们刚刚所说的,从左边开始依次传入,所以这会引起歧义。
例2:
void test2(int a = 9. int b = 4, int c)//error,并未从右往左依次指定缺省值
{
}
int main()
{
test2(45);//error
return 0;
}
该问题与与例1的问题一样,这里就不在赘述。
注意:
(1)缺省参数不能在函数声明和定义中同时出现。
void test(int a,int b = 7, int c = 4);//声明在test.h文件中
void test(int a, int b = 7, int c = 4)//定义在test.cpp文件中
{
}
缺省参数同时在声明和定义中出现,编译器就会报错。
(2)缺省值必须是常量或者全局变量。
函数重载是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来理实现功能类似数据类型不同的问题。
(1)参数个数不同
#include
using namespace std;
void test1(int a,int b)
{
cout << "void test1(int a,int b)" << endl;
}
int test1(int a)
{
cout << "int test1(int a)" << endl;
return a;
}
int main()
{
//根据参数个数不同
test1(4,7);//调用 void test1(int a,int b)函数
test1(24);//调用 int test1(int a)函数
return 0;
}
(2)参数类型不同
#include
using namespace std;
double test2(int a, double b)
{
cout << "void test2(int a,double b)" << endl;
return 4.1;
}
int test2(int a, float f)
{
cout << "int test2(int a, float f)" << endl;
return 6;
}
int main()
{
//根据参数类型不同
test2(14, 2.2);//调用 double test2(int a,double b)函数
test2(24, 29.4);//调用 int test2(int a, float f)函数
return 0;
}
(3)类型顺序不同
#include
using namespace std;
void test3(int a, double b)
{
cout << "void test3(int a,double b)" << endl;
}
void test3(double b,int a)
{
cout << "void test3(double b,int a)" << endl;
}
int main()
{
//根据参数类型不同
test3(14, 2.2);//调用 void test3(int a, double b)函数
test3(6.4, 44);//调用 void test3(double b,int a)函数
return 0;
}
要运行一个程序时,需要经过【翻译环境】和【运行环境】,我们主要讨论的是【编译环境】。【翻译环境】分为【编译(编译器)】+ 【链接(链接器)】这两个过程,【编译(编译器)】又分为(预处理、编译、汇编)三个过程。
当一个或多个 .cpp源文件经过编译(编译器)后就会生成 一个或多个.obj目标文件。然后链接器将.obj目标文件进行链接,若.obj目标文件中使用到了库中的函数,那么链接器就会到库中去寻找对应的库函数,一起进行链接。最后生成 .exe可执行程序(在Linux下是a.out)。
上面这些都还好理解,那假如test1.cpp中使用到了test2.cpp中的Fun函数,这个Fun函数还进行了函数重载,那编译器又是如何去teste2.cpp中寻找这个重载函数的?
函数重载的关键点是【链接】,这一阶段。当链接器进行链接的时候,看到test1.obj中调用到了Fun函数,但是没有Fun的地址,就会到test2.obj的符号表中去找Fun的地址,然后将两个.obj目标文件的符号表进行合并就生成了最后的.exe可执行文件。
下图是【翻译坏境】的具体过程:
那链接时,面对Fun函数,链接接器会使用哪个名字去找呢?其实每个编译器都有自己的
函数名修饰规则。由于Windows下vs的修饰规则过于复杂,而Linux下g++的修饰规则简单易懂,所以下面我们就使用Linux下的g++来演示这个修饰后的名字。
结论:在Linux下,采用g++编译完成后,函数名字的修饰发生改变,编译器将函数参数类型信息添加到修改后的名字中。
(2)采用C语言编译器编译后:
结论:在Linux下,采用gcc编译完成后,函数名字的修饰没有发生改变(即还是原名)。
说明:
(1) 通过上面我们可以看出gcc的函数修饰后名字不变。而g++的函数修饰后变成【_Z+函数长度+函数名+类型首字母】。
(2)到这里就理解了C语言为什么不支持重载,因为同名函数没办法区分。而C++是通过函数修饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。
(3)需要注意:如果两个函数的函数名和参数都是一样,而返回值不同是不构成重载的,因为调用时编译器修饰名字时,并没有将返回值的类型加入到修饰规则中,所以这是没法构成重载的。
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数可以提升程序运行的效率。即函数的压栈开销会被省去,直接执行函数体的代码。如下面的例子:
一般的函数:函数前面无任何修饰。
int add(int a,int b)//无任何修饰
{
return a+b;
}
int main()
{
int ret = add(2,6);
return 0;
}
内联函数:在函数最前面加上 inline 关键字。
inline int add(int a,int b)//inline修饰
{
return a+b;
}
int main()
{
int ret = add(2,6);
return 0;
}
在上面的例子中,当函数被 inline 所修饰后,此时该函数就是一个内联函数。并且该函数的内容十分简单。
注意: inline不建议声明和定义分离,在函数声明处添加 inline关键字虽然没有错,但这种做法是无效的,编译器会忽略函数声明处的 inline 关键字。所以建议在定义处添加 inline 。
当成为内联函数后在编译期间编译器会用 “函数体” 替换函数的调用。函数体就是{ }里面的内容。即使用内联后,调用函数时不会建立栈帧,而是在调用的地方直接展开。
我们在vs(以2019为例)中通过调试反汇编来查看是否真的如此。因为代码在release模式会被优化,这里就以Debug模式来做对比。因为在Debug模式下,编译器默认不会对代码进行优化,所以需要对编译器进行设置,否则不会展开,下面给出vs2019的设置方式:
第一步:
第二步:
第三步:
设置完成后我们在Debug下进入反汇编。
当函数不是内联函数时:
当是内联函数时:
可以看出,当一个函数是内联函数时,在编译期间会省去调用函数的开销。即使用内联以后,调用函数时不会建立栈帧,而是在调用的地方直接展开。
说明:
(1)inline是一种以空间换时间的做法,内联函数在编译阶段,会用函数体替换函数调用,缺陷就是可能会使目标文件变大,而优势则是少了调用开销,提高程序运行效率。
(2) inline只是向编译器发出一个请求,编译器可以选择忽略这个请求,不同编译器关于inline实现机制可能不同。一般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数。很多编译器都不支持内联递归函数,而且一个70多行的函数也不大可能在调用点内联地展开。
引用不是定义一个新的对象,相反的,它只是给已存在的对象另起一个名字,即起别名。编译器不会为引用开辟内存空间,它和它引用的对象共用同一块内存空间。如:李明在家被叫明儿,在江湖上被叫小明,原名叫李明,而这些名字都是另起的名字,尽管名字不同,但他们都是同一个人。相当于所谓的,已存在的对象就是李明,起别名就是被叫明儿或小明,共用同一块内存空间就是同一个人。 所以当我们操作引用的时候其实就是在操作原来的对象。
语法:类型& 别名 = 引用的对象
int a = 10;//一个整型变量,名字为a,该变量可理解为就是对象!
int& k = a;//定义一个引用
引用的对象是a,我们给a起了个别名叫k,我们今后对k的操作就是对a操作。我们需要注意:引用类型必须和引用的对象是同种类型的。
int x = 20;
char& c = x;//error,类型不一致
(1) 引用在定义时必须初始化。
int a = 3;
int& m = a;//OK,定义引用时初始化
int b = 5;
int& n;//error,定义引用时未初始化
(2)一个变量可以有多个引用。
int a = 10;
//OK
int& x = a;
int& y = a;
int& z = a;
//相当于给a起了三个不同的别名
(3)引用一旦引用一个对象,就不能再引用其他对象。
//定义了两个变量
int a = 10;
int b = 20;
int& e = a;// 这里e引用了一个对象a
e = b;//这里并非e再引用b,这里只是把b赋值给e。
当我们打印e和a的时候就会发现都等于20,这是因为b赋值给了e,而对e的改变就是对a的改变。这一点需要注意。
const关键字修饰符。所谓“修饰符”,就是在进行编译的过程中,给编译器一些“要求”或“提示”,而被const修饰的对象,具有“只读”的特点。一旦我们试图去改变这些东西,编译器就应该给出错误提示。如下:
未被const修饰的对象:具有“可读可修改”权限。
int a = 10;
a = 20;//OK,可修改
a = 30;//OK,可修改
std::cout << a << std::endl;//OK,可读
被const修饰的对象:具有“只读”权限。
const int b = 10;
b = 20;//error,不具有可修改权限
std::cout << b << std::endl;//OK,具有可读权限
当我们用const来修饰引用后,原理也是如上面的两个例子一样的。
例1:权限对等
//两者具有同样的权限
int a = 7;//a具有可读可修改权限
int& reta = a;//要引用的对象a具有可读可修改权限,
//reta也具有可读可修改权限,所以可以引用成功
//两者具有同样的权限
const int b = 3;//b只具有只读权限
const int& retb = b;//要引用的对象b只具有只读权限,
//retb也只有只读权限所以可以引用成功
b = 33;//error,不具有修改权限
retb = 333;//error,不具有修改权限
例子1中可得出结论:当引用和被引用的对象具有同等权限时,可以进行引用。
例2:权限缩小
int a = 2;//a具有可读可修改权限
const int& ret = a;//ret只具有可读权限,而a仍然具有可读可修改权限。
//因为我们const修饰的是ret,并不影响a。
ret = 20;//error,不具有可修改权限
a = 5;//OK
例子2中可得出结论:当引用后的权限比被引用的对象权限小时,可以进行引用。
例3:权限放大
const int a = 2;//只具有可读权限,不可修改
int& ret = a;//error,因为a只有只读权限,而这里的 ret 想引用a,
//如果引用成功那么相当于是修改a了,所以像这样的权限放大是不允许的。
例子3中可得出结论:当引用后的权限比被引用的对象权限大时,不可以进行引用。
例4:字面常量的引用
int& a = 10;//error,10是字面常量,只有可读权限,
//当我们未加上const修饰并试图引用一个常量时就会报错,相当于例3中的权限放大。
const int& b = 20;//OK,20是字面常量,只有可读权限,
//当我们加上const修饰后就可以进行引用,相当于例1中的权限对等。
通过上面几个例子可以总结出一句话:引用的原则:对原对象的引用不能权限放大,只能对等或者缩小。
(1)做函数参数
#include
void Swap(int& a, int& b)//a和b分别是x和y的别名
{
int temp = a;
a = b;
b = temp;
}
int main()
{
int x = 3;
int y = 7;
Swap(x,y);
return 0;
}
在C语言中我们需要交换两个数时需要传指针,而这里通过用引用做参数,也可以实现两个数交换的效果,如此一来便无需在传指针。这样用引用做参数比指针方便、简洁、好理解多了。
(2)做返回值
int& add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = add(6, 1);
add(9,4);
cout << "Add(6,1) is :"<< ret <<endl;
return 0;
}
当我们打印时,结果是13。这是因为:
(1)第一次add函数运行结束后,该函数对应的栈空间就被回收了,即c变量没有使用权了,而在main函数中ret却引用着add函数的返回值,实际上是引用着一块已经被释放的空间。
(2)第二次add函数运行结束后,同样的函数的栈空间被回收,即c变量也没有使用权了,注意空间被回收不是空间不存在了,而是我们没有使用权了,而ret引用c的位置被改成了13,因此ret的值也就跟着改成了13。
引用的是一块已被回收的空间,当该空间的内容被修改时我们的引用也跟着改变,这是极其危险的,所以我们需要注意:如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。
(1)以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效
率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
#include
#include
struct s
{
int a[100000];
};
void Func1(s a) {}
void Func2(s& a) {}
void Test()
{
s a;
// 以值作为函数参数
size_t begin1 = clock();
for (size_t i = 0; i < 10000; ++i)
Func1(a);
size_t end1 = clock();
// 以引用作为函数参数
size_t begin2 = clock();
for (size_t i = 0; i < 10000; ++i)
Func2(a);
size_t end2 = clock();
// 分别计算两个函数运行结束后的时间
cout << "Func1(A)-time:" << end1 - begin1 << endl;
cout << "Func2(A&)-time:" << end2 - begin2 << endl;
}
int main()
{
Test();
return 0;
}
从结果可以看出,以引用做函数参数时函数结束的运行时间比以值做参数的效率高上很多。
#include
#include
struct s
{
int a[10000];
};
s a;
s F1() // 值返回
{
return a;
}
s& F2() // 引用做返回
{
return a;
}
void Test()
{
// 以值作为函数的返回值类型
size_t begin1 = clock();
for (size_t i = 0; i < 100000; ++i)
F1();
size_t end1 = clock();
// 以引用作为函数的返回值类型
size_t begin2 = clock();
for (size_t i = 0; i < 100000; ++i)
F2();
size_t end2 = clock();
// 计算两个函数运算完成之后的时间
cout << "F1 time:" << end1 - begin1 << endl;
cout << "F2 time:" << end2 - begin2 << endl;
}
int main()
{
Test();
return 0;
}
通过上述代码的比较,发现传值和引用在作为传参以及返回值类型上效率相差是很大的,所以我们应该尽可能的多使用引用来提高我们的运行效率,当然我们也别忘了注意引用使用场景的注意点。
(1)在语法概念层面上引用其实就是个别名,没有开辟独立的空间,和其引用的对象共用同一块空间。
#include
int main()
{
int n = 10;
int& rn = n;
cout<<"&n = "<<&n<<endl;
cout<<"&rn = "<<&rn<<endl;
return 0;
}
(2)在引用的底层实现实际是有空间的,因为引用是按照指针方式来实现的。
我们来看引用和指针的汇编代码对比:
(3)引用和指针的不同点:
1.没有NULL引用,但有NULL指针.
2. 访问对象方式不同,指针需要解引用,引用编译器自己处理。
3. 引用在定义时必须初始化,指针没有要求。
4. 有多级指针,但是没有多级引用。
5. 引用在初始化时引用一个对象后,就不能再引用其他对象,而指针可以在任何时候指向任何
一个同类型对象。
6. 引用概念上定义一个变量的别名,指针存储一个变量地址。
7. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32
位平台下占4个字节)。
8. 用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小。
9. 引用比指针使用起来相对更安全。
本篇关于C++入门基础文章到这里就结束了,文中有不足的地方欢迎各位补充!后续我还会更新更多关于C++的文章,你的三连,是我最大的动力,感谢!