1.命名空间
使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
1°定义
定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员。
- 普通的命名空间
namespace N1
{
int a;
int Add(int left, int right)
{
return left + right;
}
}
- 命名空间可以嵌套
namespace N2
{
int a;
int b;
int Add(int left, int right)
{
return left + right;
}
namespace N3
{
int c;
int d;
int Sub(int left, int right)
{
return left - right;
}
}
}
-
同一个工程中允许存在多个相同名称的命名空间
编译器最后会合成同一个命名空间中(可以写两个N1)
namespace N1
{
int Mul(int left, int right)
{
return left * right;
}
}
2°使用
- 加命名空间名称及作用域限定符 ps: N::a
- 使用using将命名空间中成员引入 ps: using N::b
- 使用using namespace 命名空间名称引入 ps: using namespace N;
输入输出 cin和cout都在std这个命名空间内
#include <iostream>
using namespace std;
int main()
{
//1.std::不用using namespace std endl换行
std::cout << "hello world"<<std::endl;
int i = 1;
double d = 1.11;
cin >> i >> d;
//2.加了using namespace std 不需要std::
cout << i << " " << d << endl;
return 0;
}
2.缺省参数
1°概念
缺省参数是声明或定义函数时为函数的参数指定一个默认值。在调用该函数时,如果没有指定实参则采用该默认值,否则使用指定的实参。
例如:
#include <iostream>
using namespace std;
void Func(int a = 0)
{
cout << a << endl;
}
int main()
{
Func(10);//10
Func();//0
}
2°分类
- 全缺省参数
- 半缺省参数
#include <iostream>
using namespace std;
//全缺省 所有参数都给
//可以传1个 也可以传多个
void Func1(int a = 10, int b = 20, int c = 30)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
}
//半缺省 给部分参数
//必须传1个(没有缺省的位置必须传) 也可以多个
//必须从右往左连续缺省 右边的必须先给值
void Func2(int a, int b = 10, int c = 20)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
}
int main()
{
Func1();//10 20 30
Func1(1);//1 20 30
Func1(1, 2);//1 2 30
Func1(1, 2, 3);//1 2 3
Func2(1);//1 10 20
Func2(1, 2);//1 2 20
Func2(1, 2, 3);//1 2 3
}
3°注意
- 半缺省参数必须从右往左连续给出 不可以间隔
- 缺省参数不可以在函数的声明和定义中同时出现 编译器无法确定给哪个缺省值
- 缺省值必须是常量或者全局变量
- C语言不支持(编译器不支持)
3.函数重载
1°概念
- 一个函数可以有多个意义
- 要求:函数名相同 参数不同(类型/个数/顺序不同)
- 不要参数也可以 返回值相同和不同都可以
注意:只有返回类型不同不能构成重载 必须有参数不同
void Func()
int Func()
short Add(short left, short right)
{
return left+right;
}
int Add(short left, short right)
{
return left+right;
}
都不构成函数重载
2°名字修饰
为什么C语言不支持函数重载 而C++支持呢?
在C/C++中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接。
编译链接过程
- 预处理 -> 头文件的展开/宏替换/条件编译/去掉注释 list.h list.c test.c
- 编译 -> 检查语法生成汇编代码 list.i test.i
- 汇编 -> 汇编代码转成二进制的机器码 list.s test.s
- 链接 -> 将两个目标文件链接到一起 list.o test.o
编译这一步时
- C:在linux下,采用gcc编译完成后,函数名字的修饰没有发生改变。
- C++:在linux下,采用g++编译完成后,函数名字的修饰发生改变,编译器将函数参数类型信息添加到修改后的名字中。
原因
- C语言没办法支持重载,因为同名函数没办法区分。(编译器在编译.c文件时,只会给函数进行简单的重命名)
- C++是通过函数修饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。
3°extern "C"
有时候在C++工程中可能需要将某些函数按照C的风格来编译,在函数前加extern "C",意思是告诉编译器,将该函数按照C语言规则来编译。
4°练习
- 下面两个函数能形成函数重载吗?有问题吗或者什么情况下会出问题?
void TestFunc(int a = 10)
{
cout<<"void TestFunc(int)"<<endl;
}
void TestFunc(int a)
{
cout<<"void TestFunc(int)"<<endl;
}
不形成函数重载 因为两个函数的参数相同
- C语言中为什么不能支持函数重载?
C语言没办法支持重载,因为同名函数没办法区分。(编译器在编译.c文件时,只会给函数进行简单的重命名)
C++是通过函数修饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。
- C++中函数重载底层是怎么处理的?
在.cpp文件中,虽然两个函数的函数名一样,但是他们在符号表中生成的名称不一样。
- C++中能否将一个函数按照C的风格来编译?
可以 在函数前加extern "C"
4.引用
1°概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间
#include <iostream>
using namespace std;
int main()
{
int a = 1;
int& ra = a;//ra是a的引用 引用也就是别名 a再取了一个名称ra
int& b = a;
int& c = b;
//4个名字公用一块空间
//改变任何一个别名的值 每个别名和变量本身都会改变
c = 10;
//10 10 10 10
cout << a << " " << ra << " " << b << " " << c << " " << endl;
}
注意:引用类型必须和引用实体是同种类型的
2°引用特性
- 引用在定义时必须初始化
- 一个变量可以有多个引用
- 引用一旦引用一个实体,再不能引用其他实体
#include <iostream>
using namespace std;
int main()
{
int a = 1;
//int& b; //引用必须在定义时初始化
int& c = a;
int d = 2;
c = d;
//分析:这里是c变成了d的引用? no
//还是d赋值给c? ok
//说明c已经是a的引用了 不会再是其他变量的引用
//结论:引用一旦引用一个实体,再不能引用其他实体
return 0;
}
3°常引用
#include <iostream>
using namespace std;
int main()
{
//int a = 0;
//int& b = a;//b的类型是int
//如果
//const int a = 0;
//int& b = a;//b的类型是int 编译不通过 原因:a是只读 b的类型是int 可读 可写 所以不行
const int a = 0;
const int& b = a;
int c = 1;
int& d = c;
const int& e = c;
//行不行?可以的-> C可读可写 e变成别名只读
//总结:引用取别名时 变量访问的权限可以缩小 不能放大
//取别名变量范围小
return 0;
}
别名是可读可写 如果变量只可读 那么取别名编译失败
别名的权限较小
进阶:隐式类型转换+取别名
#include <iostream>
using namespace std;
int main()
{
int i = 0;
double d = i;//隐式类型转换
//double& rb = i;
//float& rf = i;
//跟字节大小无关
//无法通过编译
//但加const 可以
const double& rd = i;
const float& rf = i;
//为什么?
//i->double临时变量->db/rd/rf
//临时变量具有常性(const只可读)
//所以不加const就放大了权限 加const就可以成立
//变量之间赋值没有权限缩小和放大的关系 引用才有
const int ci = i;
int x = ci;
return 0;
}
- 引用:
一个int变量给到一个double变量
int变量->double临时变量(临时变量具有常性)->取别名的时候只能是const类型的
权限不能放大
- 赋值:
都可以 没有权限问题
4.使用场景
1°作参数
void swap_c(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//r1是a的别名 r2是b的别名 r1和r2的交换就是a和b的交换
void swap_cpp(int& r1, int& r2)
{
int tmp = r1;
r1 = r2;
r2 = tmp;
}
2°作返回值
#include <iostream>
using namespace std;
//返回一个临时变量(tmp) tmp的类型是int tmp的值是n的值
//这里会多产生一个tmp的空间
int Count1()
{
static int n = 0;
n++;
return n;
}
//这个时候tmp就是n的别名 返回的就是n
//这里不会产生多的空间
int& Count2()
{
static int n = 0;
n++;
return n;
}
int main()
{
//int& r1 = Count1();编译不通过 返回的是tmp(临时变量) 临时变量具有常性 只可读 给到可读可写 不行
const int& r1 = Count1();
int& r2 = Count2();//返回的就是tmp(n的别名) 不是临时变量
return 0;
}
- 传值返回和隐式类型转换都先要产生一个临时变量(拷贝) **临时变量具有常性 **
- 此时传值返回直接取别名可能有权限放大问题
- 此时隐式类型转换取别名不加const会有问题
但引用返回就规避了问题 因为引用和原本变量是同一空间 可以理解就是返回原来的变量
注意:
int& Add(int a, int b)
{
int c = a + b;
return c;
//局部变量(栈上) 返回后 栈销毁了
//返回c的别名
}
int main()
{
//返回之后用了c的别名 再取一个别名ret
//栈已经销毁还给系统 还去使用这块空间 会出现随机值
int& ret = Add(1, 2);
cout << "Add(1, 2) is :" << ret << endl;
return 0;
}
此时c是引用返回 但c是局部变量(栈上) 出了Add函数作用域后 栈销毁了 还给了系统
此时还去使用这块空间 出现随机值
解决方法:
- 如果还想要引用返回 那就加上static(static int c) 这样c的空间不会出函数作用域就销毁
- 使用传值返回
注意当使用static时(那条语句只会被执行一次):
int& Add(int a, int b)
{
static int c = a + b;
//这条语句只会被执行一次(第二次及以后调用函数不会再次执行)
//只会被定义一次变量
return c;
}
int main()
{
int& ret1 = Add(1, 2);
int& ret2 = Add(3, 4);//就算再调用一次 c还是3
cout << "Add(1, 2) is :" << ret1 << endl;//3
cout << "Add(3, 4) is :" << ret2 << endl;//3
return 0;
}
如果函数返回时,出了函数作用域,如果返回对象还未还给系统,则可以使用引用返回,如果已经还给系统了,则必须使用传值返回
什么变量可以使用引用返回:全局变量 静态变量
3°传值vs传引用
- 传值:会产生一份临时拷贝并返回
- 传引用:直接返回本来变量
- 传值效率更低(当参数或者返回值类型非常大)
那么函数使用引用返回好处是什么?->少开辟一个拷贝变量的空间 提高程序的效率
作参数时
#include <iostream>
#include <time.h>
using namespace std;
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();
return 0;
}
作为返回类型
#include <iostream>
#include <time.h>
using namespace std;
struct A
{
int a[10000];
};
//40000byte
//值返回
A TestFunc1(A a)
{
return a;
}
//引用返回
A& TestFunc2(A& a)
{
return 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();
return 0;
}
4°引用vs指针
初始化 用法 sizeof +- 安全等方面
不同:
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
- 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全
5.内联函数
1°概念
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率。
2°特性
- inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环/递归的函数不适宜使用作为内联函数。
- inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等等,编译器优化时会忽略掉内联。
- inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
- 一般内联适用于小函数 小于20行
#include <iostream>
using namespace std;
//在前面加inline(内联) 空间换时间
//原:Call Swap(1w)+3行指令
//现:Call Swap(1w*3) 直接全部展开了 空间变大了 但时间减少了
inline void Swap(int& x1, int& x2)
{
int tmp = x1;
x1 = x2;
x2 = tmp;
}
//频繁调用Swap 调用函数需要建立栈帧 是有消耗的
//如何解决:1.C语言使用宏函数 2.C++使用内联函数
int main()
{
int a = 0;
int b = 2;
Swap(a, b);
return 0;
}
3°练习
宏的优缺点?
优点:
- 增强代码的复用性。
- 提高性能。
缺点:
- 不方便调试宏。(因为预编译阶段进行了替换)
- 导致代码可读性差,可维护性差,容易误用。
- 没有类型安全的检查 。
C++有哪些技术替代宏?
- 常量定义 换用**const ** #define N 10; -> const int N = 10;
- 函数定义 换用内联函数
6.auto关键字(C++11)
1°简介
C++11:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。
int a = 0;
auto b = a;
此时b的类型就会根据a的类型推导出来
2°使用
- auto与指针和引用结合起来使用
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
#include <iostream>
using namespace std;
int main()
{
int a = 0;
auto b = a;//b的类型是根据a的类型推导出是int
int& c = a;
auto& d = a;//引用类型加上&
auto* e = &a;
auto f = &a;//不加*也可以推出来指针 指针可以不加
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
cout << typeid(e).name() << endl;
cout << typeid(f).name() << endl;
//auto实际作用
//类型很长可以直接写成auto
//使用auto来进行优化 简化代码的方法
return 0;
}
- 在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
auto a=1,b=2;
auto c=3,d=4.0;
第二行编译失败 因为auto已经是int类型了 auto d = 4.0不可以
3°auto不能推导的情况
参数和数组不能用auto
auto不能作为形参类型
auto不能直接用来声明函数
7.范围for循环(C++11)
C++98:for(int i = 0;i<sizeof(arr)/sizeof(arr[0]);++i)
C++11:for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
#include <iostream>
using namespace std;
int main()
{
int array[] = { 1,2,3,4,5 };
//要求将数据中的值乘2倍 再打印一遍
//C语言的做法
for (int i = 0; i < sizeof(array) / sizeof(int); ++i)
{
array[i] *= 2;
}
for (int i = 0; i < sizeof(array) / sizeof(int); ++i)
{
cout << array[i] << " ";
}
cout << endl;
//C++11->范围for(语法糖)->特点:写起来比较简单
//e是别名 e的改变就是数组的改变
//可以看成array中每个元素取出来赋值给e
//e的改变不会影响array中元素的改变
//e如果是别名就可以
for (auto& e : array)
{
e *= 2;
}
for (auto e : array)
{
cout << e << " ";
}
cout << endl;
return 0;
}
注意: for循环迭代的范围必须是确定的
void TestFor(int array[]) { for (auto& e : array)//这里传参 array是首元素地址 无法通过编译 cout << e << endl; }
这里是首元素地址 for循环范围不确定
8.指针空值nullptr(C++11)
C++98: int* p = NULL;
NULL实际是一个宏
stddef.h中:
#ifndef NULL #ifdef __cplusplus #define NULL 0 #else #define NULL ((void *)0) #endif #endif
NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。
字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量。
但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。
这里就会遇到麻烦 你想的是指针 但编译器会默认为整形常量
所以C++11引用nullptr关键字
- 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
- 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
- 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。
【C++】1.C++基础 完