C++基础:
- 1. 命名空间
- 命名空间如何定义
- 命名空间如何使用
- 2.缺省参数
- 2.1 缺省参数分类
- 2.1.1全缺省参数
- 2.1.2 半缺省参数
- 3. 函数重载
- 4. 引用
- 4.1 常引用
- 4.2 引用使用场景
- 4.2.1 做参数
- 4.2.2 做返回值
- 4.2.3 传值和传引用效率分析
- 4.3 引用和指针的区别
- 4.4 指针和引用总结
- 5. 内联函数
- 5.1 特性
- 5.2 面试题
- 6.auto关键字
- 6.1 auto使用方法
- 6.2 auto不能使用的场景
- 7.基于范围for的循环表达式
- 7.1 范围for使用方法
1. 命名空间
在C/C++中,变量、函数和类都是大量存在的,它们的名称都将存在于全局作用域中,可能会导致很多冲突。如下图:
因为time函数在全局作用域中,自己又在全局范围内声明了一个time对象,导致函数名与对象名重名,造成错误
为此产生了命名空间的概念:需要使用namespace关键字,后面接命名空间的名字,然后接一堆{}即可,{}中为命名空间的成员。
#include<iostream>
using namespace std;//编译不通过
int time = 1;
int main()
{cout << "hello world!" << endl;cout << time << endl;return 0;
}
//错误C2365“time” : 重定义;以前的定义是“函数” #include<iostream>
using namespace std;
//声明一个命名空间xty,使自己声明的变量在xty作用域,和全局作用域分开,因此
//使用时需要在前面加一个xty::表明去xty命名空间去找
namespace xty
{int time = 1;
};
int main()
{cout << xty::time << endl;return 0;
}
// 可以运行//输出为1
编译运行时,如果遇到未知的符号,没有特别的声明时,默认会先到全局作用域去寻找,因此在使用时,尽量声明它所在的作用域。
命名空间如何定义
#include<iostream>
using namespace std;//声明一个命名空间xty1
namespace xty
{int time = 1;void PRINT(){cout << "xty" << endl;}
};//命名空间可以嵌套定义
namespace ljj
{int time = 2;namespace ljj_PRINT {void PRINT(){cout << "ljj" << endl;}};
};//同一个工程中允许存在多个相同的命名空间,比如a.cpp,b.cpp,a.h,b.h都又相同的命名空间xty
//编译器在链接时,会将它们自动合并成一个命名空间xtyint main()
{// 命名空间的使用//正常使用cout << xty::time << endl;xty::PRINT();//嵌套使用cout << ljj::time << endl;ljj::ljj_PRINT::PRINT();return 0;
}
命名空间如何使用
例子按上面代码来探究:
- 加命名空间名称+作用域限定符::
int main ()
{//正常使用cout << xty::time << endl;xty::PRINT();return 0;
}
- 使用using将命名空间的某个成员引入
// 仅仅将xty中的time成员引入,这样使用time就不用加域作用限定符了
using xty::time
int main ()
{//正常使用cout << time << endl;xty::PRINT();return 0;
}
- 使用using namespace 命名空间名称 引入
// 将整个命名空间展开
//这样就可以不加域作用限定符使用xty下的所用成员
using namespace xty;
int main ()
{//正常使用cout << time << endl; PRINT();return 0;
}
先在全局作用域中去寻找,如果没有再在展开的命名空间中去寻找
如下例子:
#include<iostream>
using namespace std;//声明一个命名空间xty
namespace xty
{int time = 2;void PRINT(){cout << "xty" << endl;}
};
// 展开声明的命名空间
using namespace xty;int main()
{cout << ::time << endl;return 0;
}// 结果为time函数的地址
因为C++规定所有库函数都需要写在std的命名空间里面,using namespace std 仅仅是把std展开,只是一个命令,仍然需要把库函数头文件引进来!!保证有这个文件了,再将这个文件的std展开,就可以了。
2.缺省参数
缺省参数就是在声明或定义函数时,为函数的参数指定一个缺省值,当调用函数时,如果没有指定该参数,则默认使用缺省值。
#include<iostream>
using namespace std;//设置缺省值为10
void Text(int a = 10)
{cout << a << endl;
}
int main()
{Text();//输出结果为10Text(20);//输出结果为20return 0;
}
2.1 缺省参数分类
2.1.1全缺省参数
所有参数都缺省
#include<iostream>
using namespace std;//全缺省参数
void Text(int a = 10, int b = 20, int c = 30)
{cout << a << endl;cout << b << endl;cout << c << endl;
}
int main()
{Text();//输出结果为10,20,30//参数必须从左往右依次给出Text(1, 2);//输出结果 1,2 ,30Text(5);//输出结果5,20,30//必须从左往右给,传参错误!Text(, 0);return 0;
}
2.1.2 半缺省参数
只有部分参数缺省
#include<iostream>
using namespace std;//半缺省参数
//缺省参数必须是从右往左连续的!
void Text(int a, int b = 2, int c = 3)
{cout << a << endl;cout << b << endl;cout << c << endl;
}
int main()
{Text(10);//输出结果为10,2,3return 0;
}
注意:1.缺省参数不能在函数声明和定义同时出现;同时出现时,声明说了算,要将定义处缺省删去。2.缺省值必须是常量或者是全局变量
3. 函数重载
C++允许在同一个作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数或类型或类型顺序)不同,常用来处理实现功能类似但数据不同任务。
#include<iostream>
using namespace std;// 函数重载
int Add(int x, int y)
{return x + y;
}//1. 参数类型不同
double Add(double x, double y)
{return x + y;
}//2.参数数量不同
int Add(int x, int y, int z)
{return x + y + z;
}// 3.参数类型顺序不同
int Add(int x, double y, int z)
{return x + y + z;
}
int Add(int x, int y, double z)
{return x + y + z;
}int main()
{//感觉调用了一个函数一样,但是传进去了不同的数据类型//完成了不同的加法任务Add(1, 2);Add(1.11, 2.22);Add(1, 2, 3.33);return 0;
}
key:只有参数类型或参数顺序或参数个数不同才构成重载,返回值不同不构成重载!!!
4. 引用
引用就是给一个变量取别名。
使用符号&
类型& 引用变量名(对象名)= 引用实体
引用的类型必须和引用实体的类型相同
int main()
{int x = 10;//引用必须在声明的时候初始化,即声明的时候就得指出要给谁起别名。// int& y;int& y = x;cout << x << endl;cout << y << endl;//结果都是10return 0;
}
引用特性:1.引用一旦声明就必须初始化;2.一个变量可以有多个引用(一个变量可以有多个别名);3.引用一旦引用一个实体,就不能在引用其他实体
4.1 常引用
void Text()
{const int a = 1;//编译会出错,a本身为常量,因为区别名后权限放大//int& ra = a;const int& ra = a;//因为b为常量,编译的时候同样会出错//int& b = 10;const int& b = 10; //正确写法double c = 1.11;//该语句编译时会出错,类型不同//int& rc = c;const int& rc = c; //涉及到整型提升的问题,临时变量是右值,不可修改,因此const修饰能过int ii = 1;double dd = ii;//涉及到整型提成,dd实际存储的是临时变量,临时变量具有常性//double& rdd = ii; //编译不过,因为临时变量具有常性,放大权限了const double& rdd = ii;//可以编过。}
强制类型转换、整型提升,产生的都是临时变量,临时变量具有常性;如果在原地改变,有可能空间不够!!!
4.2 引用使用场景
4.2.1 做参数
//引用传值
void Swap(int& a, int& b)
{int tem = a;a = b;b = tem;
}//传指针
void Swap(int* a, int* b)
{int tem = *a;*a = *b;*b = tem;
}
//对比发现,传引用比传指针更方便,代码量更少,更安全
4.2.2 做返回值
// 引用做返回值
//直接返回别名,可以直接改
int& Func()
{static int n = 0;//...n++;return n;
}//传指返回
int Func()
{int n = 0;//...n++;return n;
}
函数返回时,会把当前函数的栈帧清空,返回值存储到一个临时的寄存器中,用来返回给调用者;如果不存到寄存器中,栈帧清空后,才返回到调用者函数,会导致原函数的数据先被OS清空,这样会丢失数据。因此,必须有一个能存储临时变量的寄存器。
使用引用作返回值的前提是:函数返回时,返回的变量不能还给操作系统,否则不能用引用返回,必须使用传值返回
4.2.3 传值和传引用效率分析
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
下面是一组代码用来测试传值和传引用的效率比较:
#include <time.h>
struct A { int a[10000]; };
void TestFunc1(A a) {}
void TestFunc2(A& a) {}
void TestRefAndValue()
{A a;// 以值作为函数参数size_t begin1 = clock();for (size_t i = 0; i < 10000; ++i)TestFunc1(a);size_t end1 = clock();// 以引用作为函数参数size_t begin2 = clock();for (size_t i = 0; i < 10000; ++i)TestFunc2(a);size_t end2 = clock();// 分别计算两个函数运行结束后的时间cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
int main()
{TestRefAndValue();//cout << "hello world!" << endl;return 0;
}
下面一组代码用来测试返回值和返回引用的效率比较:
#include <time.h>
struct A{ int a[10000]; };
A a;
// 值返回
A TestFunc1() { return a;}
// 引用返回
A& TestFunc2(){ return a;}
void TestReturnByRefOrValue()
{// 以值作为函数的返回值类型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 time:" << end1 - begin1 << endl;cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
4.3 引用和指针的区别
在语法概念上引用就是一个别名,没有独立空间,和引用的实体共用同一块空间。
int a = 10;int* pa = &a;*pa = 20;int& ra = a;ra = 20;cout << "&a = " << &a << endl;cout << "pa = " << pa << endl;cout << "&ra = " << &ra << endl;//输出的结果都是一个地址
然而他们的底层逻辑是一样的,我们看一下汇编代码:
可以发现,指针和引用的汇编代码完全一样,并没有什么不同,因此我们可以推断,引用就是使用指针实现的。
4.4 指针和引用总结
- 引用在概念上定义一个变量的别名,指针存储一个变量的地址。
- 引用在定义时初始化(必须指出引用了谁),指针没有要求。
- 引用在引用一个实体之后,不能再次改变,指针可以随意指向任意一个实体。
- 没有NULL引用,但可以有NULL指针。
- sizeof中,引用的结果始终是引用类型的大小,但指针始终是地址的字节数。
- 引用自加即实体+1,指针自加地址偏移一个类型的大小。
- 有多级指针,但是没有多级引用。指针更复杂
- 访问实体方式不同:指针需要显式的解引用,引用需要编译器自己处理。
- 引用比指针使用起来更相对安全。
5. 内联函数
内联函数是用inline关键字修饰的函数,编译时C++会在调用内联函数的地方展开,这样就没有函数调用建立栈帧的开销,可以提升程序的运行效率。用来替代C语言宏的语法
5.1 特性
- inline是一种以空间换时间的做法,如果编译器将函数当作内联函数处理,在编译阶段会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。(长函数与递归不适合内联)
- inline对于编译器只是一个建议,不同编译器关于inline实现的机制可能不同,一般建议:将函数规模小(函数不是很长)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline不作展开。
- inline不建议声明和定义分离,分离会导致链接错误。因为inline在声明处展开,没有函数地址 ,链接就会找不到。因此,声明和定义写一块。
5.2 面试题
- 宏的优缺点?答:优点:增强代码复用性;提高性能。缺点:不方便调试宏(宏在编译阶段替换掉了);导致代码可维护性差,可读性差,容易误用;无类型安全检查。
- C++有哪些技术替代宏?答:常量定义,换用const,enum;缩小函数定义换用内联函数。
6.auto关键字
使用auto关键字可以自动推到变量的类型,不需要再次写类型。
#include<iostream>
using namespace std;int main()
{int a = 10;char b = 'x';auto c = a;auto d = b;//auto 自动推测处a, b的类型,并存储它cout << typeid(c).name() << endl; // int cout << typeid(d).name() << endl; // char
}
key:使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。
6.1 auto使用方法
- auto与引用和指针可以合用
int x = 10;auto a = &x; //类型是 int*auto* b = &x; //只是强调等号右边一定填指针,b的类型也是int*auto& c = x; //给x取别名,c的类型引用
2.在同一行定义多个变量
auto a = 1, b = 2; //声明并定义,能编过auto c = 1, d = 2.2; //不能编过
当在同一行声明多个变量时,这些变量必须有相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
6.2 auto不能使用的场景
1.auto不能作为函数的参数
//此时会编译失败,auto不能用作形参类型
//因为编译器无法对a的实际类型进行推导
void test(auto a)
{;
}
2.auto不能直接用来声明数组
// 声明数组int a[] = { 1, 2, 3 }; auto b[] = { 1, 2, 3 }; //编译不过
3.auto在实际中最常见的优势用法就是新式for循环,还有lambda表达式等进行配合使用。
7.基于范围for的循环表达式
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号”:”分为两部分: 第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
for(auto 迭代变量 :被迭代的范围 )
使用范例:
int a[] = { 1,2,3,4,5 };for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++){cout << a[i] << " "; // 1 2 3 4 5}//使用auto推到的范围for//将a中的数据依次赋给datafor (auto data : a){cout << data << " "; // 1 2 3 4 5}//使用auto& 可以修改a中的数据//给a的数据取别名for(auto& data : a){data++; //此时a中的数据也被改变}//使用指针不能编译!!!for(auto data : &a){(*data)++; }
7.1 范围for使用方法
1.for循环迭代的范围必须是确定的!
void Test(int arr[])
{for (auto& data : arr) //不能编译,因为传进来的arr是地址,编译器不能分辨出这个是数组{cout<<data<<endl;}
}