1、命名空间
命名空间是用来解决命名冲突的,在一些大型的项目中难免会出现命名冲突的问题,C语言就无法解决,C++的命名空间可以很好的解决命名冲突的问题。接下来我们看一个例子:
cpp
#include <iostream>
#include <stdlib.h>
using namespace std;
int rand = 0;
int main()
{
return 0;
}
当进行编译后会发现报错了: 这里显示的是rand重定义了,之所以报错是因为stdlib.h库里有一个rand的函数,所以导致了命名冲突。那该怎么解决呢?这里就引入C++的命名空间了:
cpp
#include <iostream>
#include <stdlib.h>
using namespace std;
namespace zzy
{
int rand = 0;
}
int main()
{
return 0;
}
像这样定义了一个zzy的命名空间,这样就不会和库里的rand函数冲突了。
而当我们想访问命名空间的变量时,我们需要通过域作用限定符::来访问
cpp
#include <iostream>
#include <stdlib.h>
using namespace std;
namespace zzy
{
int rand = 0;
}
int main()
{
int rand = 1;
printf("%d\n", rand); // 优先访问局部域 -> 1
printf("%p\n", ::rand); // 访问全局域 -> rand函数的地址
printf("%d\n", zzy::rand); // 访问命名空间域zzy -> 0
return 0;
}
另外命名空间还可以定义结构体、函数,也可以嵌套命名空间,两个同名的命名空间会合并:
cpp
#include <iostream>
#include <stdlib.h>
using namespace std;
namespace zzy
{
struct Node
{
int val;
struct Node* next;
};
int add(int x, int y)
{
return x + y;
}
namespace test
{
int rand = 2;
}
}
namespace zzy
{
int rand = 1;
}
int main()
{
printf("%d\n", zzy::rand); // 输出1
printf("%d\n", zzy::test::rand); // 输出2
return 0;
}
我们也可以将命名空间展开到全局,这样就不需要通过域作用限定符访问了,但是要注意不要与全局的其他变量冲突:
cpp
#include <iostream>
#include <stdlib.h>
namespace zzy
{
int x = 1;
int y = 2;
}
using namespace zzy;
int main()
{
printf("%d %d", x, y); // 输出1 2
return 0;
}
也可以部分展开:
cpp
#include <iostream>
#include <stdlib.h>
namespace zzy
{
int x = 1;
int y = 2;
}
using zzy::x; // 只展开了x,访问y还是要通过域作用限定符来访问。
int main()
{
printf("%d %d", x, zzy::y); // 输出1 2
return 0;
}
C++库的实现定义在名为std的命名空间中,当我们要使用C++标准库的方法或者函数时就需要通过域作用限定符去访问,而我们平时一般不需要是因为我们写的代码都带有using namespace std; 相当于把std命名空间展开了。 当然我们也可以展开一部分。
cpp
#include <iostream>
#include <stdlib.h>
namespace zzy
{
int x = 1;
int y = 2;
}
using namespace zzy;
//using namespace std; 全部展开
// 部分展开
using std::cout;
using std::endl;
int main()
{
std::cout << x << " " << y << std::endl; // 不展开,通过域作用限定符访问
cout << x << " " << y << endl; // 展开到全局,直接使用。
return 0;
}
在我们做项目的时候一般推荐部分展开,把常用的展开,而我们自己平时写代码为了方便可以全部展开。
2、C++输入&输出
cout是C++的标准输出对象,cin是标准输入对象,使用cout和cin要包含头文件iostream。 endl是C++的特殊符号表示换行输出。 <<是流插入运算符,>>是流提取运算符。 使用cout和cin不需要像C语言printf和scanf指明数据类型。
而C++控制输出格式比较复杂,所以当我们需要控制输出格式时直接用printf即可。
3、缺省参数
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该 形参的缺省值,否则使用指定的实参。
可以像下面Func函数给a一个缺省值:
cpp
#include <iostream>
#include <stdlib.h>
using namespace std;
void Func(int a = 0)
{
cout << a << endl;
}
int main()
{
Func(1); // 显示传参,输出1
Func(); // 使用默认缺省值,输出0
return 0;
}
3.1、全缺省
cpp
#include <iostream>
#include <stdlib.h>
using namespace std;
void Func(int a = 0, int b = 1, int c = 2)
{
cout << a << " " << b << " " << c << endl;
}
int main()
{
Func(1); // 显示传参,输出1 1 2
Func(); // 使用默认缺省值,输出0 1 2
return 0;
}
3.2、半缺省
也可以缺省一部分,称为半缺省,注意半缺省必须从右向左缺省,因为我们给函数传参是从左往右的:
cpp
#include <iostream>
#include <stdlib.h>
using namespace std;
void Func(int a, int b = 1, int c = 2)
{
cout << a << " " << b << " " << c << endl;
}
int main()
{
Func(0); // 显示传参,输出0 1 2
return 0;
}
3.3、缺省参数的周边问题
另外缺省参数不能在声明和定义中同时出现 ,例如下面这段代码,在Stack.hpp中声明Stack的初始化函数,在Stack.cpp中定义初始化函数: 这样是编不过的,因为如果声明和定义提供的值不同,那么编译器就无法确定要使用哪个缺省值,如果只在定义中出现也是不行的,所以我们只能在声明中给出缺省值。
缺省值的用途:当我们定义一个栈结构出来,如果我们知道要用多少的容量,我们可以直接显示传参去初始化,减少后面扩容的消耗。而如果我们不知道需要多少容量我们就用缺省值。
cpp
#include <iostream>
#include <stdlib.h>
using namespace std;
struct Stack
{
int* a;
int size;
int capacity;
};
void StackInit(struct Stack* st, int capacity = 4)
{
st->a = (int*)malloc(sizeof(int) * capacity);
st->size = 0;
st->capacity = capacity;
}
int main()
{
Stack st1, st2;
StackInit(&st1, 100); // 知道要用多少数据,就显示传参
StackInit(&st2); // 不知道要用多少数据,就用默认的缺省值
return 0;
}
4、函数重载
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数、类型、类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。
例如我们要实现一个加法函数,参数既可以是int类型,也可以是double,函数重载可以让我们定义出同名但参数列表不同的函数 。其实这里用模板更好,后面会介绍。
cpp
#include <iostream>
#include <stdlib.h>
using namespace std;
// 以下两个函数构成重载
int Add(int x, int y)
{
return x + y;
}
double Add(double x, double y)
{
return x + y;
}
int main()
{
cout << Add(1, 2) << endl; // 调用int的加法函数,输出3
cout << Add(1.1, 2.2) << endl; //调用double的加法函数,输出3.3
return 0;
}
构成函数重载有三种方式:参数的个数不同,参数的类型不同,参数的类型顺序不同
4.1、函数的参数个数不同构成重载
4.2、函数的参数类型不同构成重载
4.3、函数的参数类型顺序不同构成重载
这里还要注意:返回值不同是不构成函数重载的------调用的时候无法区分。以下还有一个例子:他们构成重载但是调用会出现歧义
cpp
#include <iostream>
#include <stdlib.h>
using namespace std;
void Func()
{
cout << "void Func()" << endl;
}
void Func(int a = 0)
{
cout << "void Func(int a = 0)" << endl;
}
int main()
{
Func(1); // 传参调用不会出现歧义
Func(); // 由于有缺省值,所以调用出现歧义,但是他们是构成函数重载的
return 0;
}
4.4、函数重载的原理
接下来看看在Linux下g++编译器中函数重载的原理: C和C++程序的代码如下:
我们进行编译后通过objdump -S查看汇编代码: 可以看到C语言编译出来函数的在func.o符号表中的函数名就是Func
而C++不是直接用函数名来标识的,这里的_Z是统一的前缀,4代表的是Func的长度,i代表int,c代表的是char,所以C++中构成函数重载的函数在func.o符号表中的名字是不一样。
C语言不支持函数重载,因为编译的时候,两个重载函数函数名是一样的,在func.o符号表中存在歧义和冲突,链接的时候也存在歧义和冲突,因为C语言是直接用函数名去标识和查找的。而重载函数的函数名一样,无法区分。 C++支持函数重载,因为C++符号表中并不是直接用函数名来标识和查找函数,C++有了函数名修饰规则(不同编译器下规则不同,以上举的是g++的例子),有了函数名修饰规则,func.o符号表中重载的函数就不存在歧义和冲突,其次链接的时候查找的也是明确的。
5、引用
C++中有了引用的概念,在部分场景下可以替代指针,在语法层面上我们认为:引用是给变量起别名,并没有开辟新空间。先来看引用的语法:
cpp
类型& 引用变量名(对象名) = 引用实体;
int a = 10;
int& b = a; // 定义引用类型b,这里让b引用a,b就是a的别名
需要注意的是引用类型和引用的实体必须是同种类型的。
5.1、引用的特性
引用在定义的时候必须初始化:
cpp
int a = 10;
int& b; // -> error
int& c = a; // -> true
一个变量可以有多个引用:
cpp
int a = 10;
int& b = a;
int& c = a;
int& d = b; // 这里被a的引用b起别名,本质还是a的引用,b和d都是a的别名
引用一旦引用了一个实体,就不能再引用其他实体:
cpp
int a = 10;
int b = 20;
int& c = a;
c = b;
cout << c << endl; // 输出20
这里的c=b是把b赋值给c,还是让c称为b的别名呢?这里其实是赋值
5.2、引用传参
在C语言,我们要交换两个变量的值得通过指针来修改,有了引用之后就不需要指针了,而且传参还不用取地址,并且它们构成函数重载
cpp
#include <iostream>
#include <stdlib.h>
using namespace std;
void swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
void swap(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 0, b = 1;
int c = 1, d = 0;
swap(&a, &b); // 通过指针交换变量的值
cout << a << " " << b << endl;
swap(c, d); // 通过引用交换变量的值,不需要&
cout << c << " " << d << endl;
return 0;
}
引用做参数还可以提高效率,当传参是一个大对象或者深拷贝时,可以减少拷贝提高效率,接下来我们测试用引用传参和普通传参的效率差别:
cpp
#include <iostream>
#include <stdlib.h>
using namespace std;
#include <time.h>
struct A { int a[100000]; };
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;
}
这里我们可以看到引用传参的效率更高。
引用传参的另一个意义是作为输出型参数将结果带出:
cpp
// C语言通过指针将余数r带出
int div(int x, int y, int* r)
{
if (y == 0) return -1;
*r = x % y;
return x / y;
}
// C++通过引用将余数r带出
int div(int x, int y, int& r)
{
if (y == 0) return -1;
r = x % y;
return x / y;
}
5.3、引用作返回值
与作参数相同,引用返回可以提高效率,因为不需要拷贝。
cpp
#include <iostream>
#include <stdlib.h>
using namespace std;
#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;
}
int main()
{
TestReturnByRefOrValue();
return 0;
}
测试结果还是引用快很多,所以引用传参和作返回值都可以提高效率。
另外,引用作返回值,不仅可以获取返回值,还可以修改返回值
cpp
#include <iostream>
#include <stdlib.h>
using namespace std;
struct stack
{
int a[100];
int size;
int capacity;
int& at(int pos)
{
return a[pos];
}
};
int main()
{
stack st;
st.at(0) = 10;
return 0;
}
这里我们直接调用at函数获取0下标的元素,然后将其修改成10。由于是传引用返回,所以可以直接修改a[0]的值。在c++中结构体升级成了类,可以在类中定义成员函数和成员变量,这个我们下一章会讲解。现在我们知道引用返回可以获取返回值,还可以对返回值做修改就可以。
5.4、常引用
cpp
int a = 10;
int& b = a;
const int& c = a;
上面的a是可读可写 的,b也是可读可写 的,而c是只读 的,b就是权限的平移 ,而c是权限的缩小。
cpp
const int a = 10;
int& b = a; // error
const int& c = a;
上面的a是只读 的,而b是可读可写的,明显不合理,所以不能这么定义,这样就相当于权限的放大。 而c是只读的,相当于权限的平移,是允许的。
5.5、引用做返回值深入剖析
cpp
#include <iostream>
#include <stdlib.h>
using namespace std;
int Test()
{
int n = 0;
n++;
return n;
}
int main()
{
int ret = Test(); // 1
return 0;
}
上面是很简单的传值返回,返回n时会先把n的值拷贝给临时变量,然后再将临时变量的值拷贝给ret,一共是有两次拷贝。并且这里我们要知道临时变量具有常性------不可修改,临时变量是右值(C++11会介绍)。返回后Test函数的栈帧被销毁,里面变量开辟的空间也就不复存在了。如果返回的值比较小,是4/8个字节,会先存到寄存器中,再将寄存器的值拷贝给ret。
cpp
#include <iostream>
#include <stdlib.h>
using namespace std;
int Test()
{
static int n = 0;
n++;
return n;
}
int main()
{
int ret = Test(); // 1
return 0;
}
这时候n存储在静态区,返回n还是会先生成一个临时变量,然后再把临时变量的值拷贝给ret。只不过返回后,由于n时存储在静态区,而不是存储在栈,所以n所开辟的空间并没有被释放。
cpp
#include <iostream>
#include <stdlib.h>
using namespace std;
int& Test()
{
static int n = 0;
n++;
return n;
}
int main()
{
int ret = Test(); // 1
return 0;
}
这时候返回时会先生成n的引用作为临时变量,然后再将n的引用的值拷贝给ret,所以进行了一次拷贝。
cpp
#include <iostream>
#include <stdlib.h>
using namespace std;
int& Test()
{
int n = 0;
n++;
return n;
}
int main()
{
int ret = Test();
return 0;
}
上面的代码就是有问题的了。这时候返回先生成n的引用,然后将值拷贝给ret,但是函数调用结束会销毁栈帧,会将n开辟的空间还给内存,这时候再去访问n的值就是有问题的了。 如果栈帧销毁后没有清理栈帧,那么ret的值就是1,侥幸是正确的。 如果栈帧销毁后清理了栈帧,那么ret的值就是随机值。 所以这里ret的值是不确定的,在引用作为返回值时要注意,如果是在栈上开辟的变量,返回后就会被销毁,空间被释放,这时候返回引用就会出问题。
cpp
int& Test()
{
int n = 0;
n++;
return n;
}
int main()
{
int& ret = Test();
return 0;
}
这时候ret的值就是上面所说的不确定的。但是这里我们侧重点不在这,这里返回n先生成n的引用,然后再让ret成为n的引用的引用,所以最后ret本质还是n的引用,这里并没有发生拷贝。
cpp
#include <iostream>
#include <stdlib.h>
using namespace std;
int& Test()
{
int n = 0;
n++;
return n;
}
int main()
{
int& ret = Test();
cout << ret << endl;
printf("ssssssssssssssssss\n");
cout << ret << endl;
return 0;
}
解释:上面调用Test后栈帧销毁,但是vs并没有清理栈帧,所以ret的值侥幸是1。这时候再调用printf函数,printf函数的栈帧会覆盖之前Test函数的栈帧,所以ret的值就未知了。
总结:1、基本任何场景都可以使用引用传参。2、引用作返回值时要注意,出了函数作用域后对象不存在了,就不能引用返回,还在就可以用引用返回。
5.6、引用的原理
这是在vs下的一段代码:
cpp
#include <iostream>
using namespace std;
int main()
{
int a = 10;
int& b = a;
b = 20;
int* p = &a;
*p = 30;
return 0;
}
接下来运行后查看反汇编:
解释:首先看引用的汇编,lea相当于是取地址,先将a的地址放到eax寄存器中,然后将eax的值给b。 将b的值放到寄存器eax中,然后[eax]就相当于解引用然后赋值20,这里的14h代表十六进制,转换成十进制就是20。 再看指针的汇编代码,发现和引用是一样的,所以我们可以得出:引用的底层就是指针。
5.7、指针和引用的区别
1、指针的指向是可以改变的。引用一旦引用了一个实体,就不能再引用其他实体。 2、指针在定义的时候可以不初始化,引用定义时必须初始化 3、有空指针,但没有空引用。有多级指针,但没有多级引用 4、sizeof中含义不同,指针计算的始终是4/8个字节(取决于32/64位平台),引用计算的结果是引用类型的大小。 5、指针访问实体需要解引用,而引用不需要。 6、引用比指针更加安全
6、内联函数
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧 的开销,内联函数提升程序运行的效率。 当我们需要频繁调用一个函数时,比如Add函数:
cpp
int Add(int x, int y)
{
return x + y;
}
由于Add函数需要被频繁调用,比如要调用十万次,那么就需要不断的开辟栈帧 ,这样消耗就比较大 。在C语言我们可以通过宏来解决:
cpp
#include <iostream>
using namespace std;
#define Add(x, y) ((x)+(y))
int main()
{
int x = 10, y = 20;
cout << Add(x, y) << endl;
return 0;
}
但是宏很容易出错,比较复杂,比如上面的Add宏,x与y的一个括号都不能少 ,否则就有可能出问题。 在C++中我们可以在函数前面加上inline表示这是内联函数。
cpp
inline int Add(int x, int y)
{
return x + y;
}
在vs下面我们可以调试查看反汇编: 而不加inline是这样的:
当然VS默认查看到的还是会call一个函数,这时候需要修改两个属性,首先右击项目选择属性 ,然后修改C/C++下常规中的调式信息格式为程序数据库 ,再把优化中的内联函数扩展改为只适用于inline。修改完之后添加inline就不会看到call了。
另外inline不支持声明和定义分离,如果是.h文件和.cpp文件一起的,那就直接在.h文件中定义就好。 inline对于编译器而言只是一个建议,不同编译器对于inline的实现机制可能不同。一般我们建议对于函数规模小的,频繁调用的并且不是递归的函数添加inline。
7、auto关键字
auto关键字可以自动推导类型:
cpp
#include <iostream>
#include <stdlib.h>
using namespace std;
int main()
{
auto a = 10;
auto b = 1.11;
auto c = 'c';
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
return 0;
}
这里我们用typeid打印出类型,只需要知道用法即可。但是这里作用还不是很大,接下来举一个更好的例子,下面的例子看不懂没关系,后面会做介绍。下面的例子用auto关键字就很方便:
cpp
#include <iostream>
#include <stdlib.h>
#include <string>
#include <unordered_map>
using namespace std;
int main()
{
std::unordered_map<std::string, std::string> m;
std::unordered_map<std::string, std::string>::iterator it = m.begin();
auto mit = m.begin(); // 这里用auto关键字自动推导类型就可以很简便的写出来
return 0;
}
auto对于指针和引用:
cpp
int a = 10;
auto p = &a;
auto* pp = &a; // 指针可加*也可不加
auto& b = a; // 引用必须加&
auto在同一行声明的变量必须是相同的类型,否则会报错:
cpp
auto a = 1, b = 2;
auto c = 1, d = 1.1; // error c和d的类型不同
auto不能声明数组,也不能作为函数参数:
cpp
auto a[] = { 1, 2, 3 }; // error
void Test(auto a) // error
{
}
8、范围for
在早期C / C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量。 C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。 实际上范围for的底层是迭代器,这个我们在后面讲STL容器的时候会做介绍
在以前,我们要遍历数组和修改数据是这样写的:
cpp
#include <iostream>
#include <stdlib.h>
#include <string>
#include <unordered_map>
using namespace std;
int main()
{
int a[] = { 1, 2, 3, 4, 5, 6, 7 };
for (int i = 0; i < sizeof(a) / sizeof(int); i++)
{
cout << a[i] << " ";
}
cout << endl;
for (int i = 0; i < sizeof(a) / sizeof(int); i++)
{
a[i]++;
}
for (int i = 0; i < sizeof(a) / sizeof(int); i++)
{
cout << a[i] << " ";
}
return 0;
}
C++11有了范围for和auto关键字我们还能这么写:
cpp
#include <iostream>
using namespace std;
int main()
{
int a[] = { 1, 2, 3, 4, 5, 6, 7 };
for (auto e : a)
{
cout << e << " ";
}
cout << endl;
for (auto& e : a)
{
e++;
}
for (const auto& e : a)
{
cout << e << " ";
}
cout << endl;
return 0;
}
上面可以用auto来自动推导类型,也可以用int指明类型。但是需要注意的是:修改数据时得用引用,如果不用引用就是拷贝了,实际上并不会修改数组里的值。
cpp
#include <iostream>
using namespace std;
void Test(int* a)
{
for (auto e : a)
{
cout << e << " ";
}
cout << endl;
}
int main()
{
int a[] = { 1, 2, 3, 4, 5, 6, 7 };
Test(a);
return 0;
}
上面的代码是错误的,因为这时候a已经是指针了,不知道a的范围,范围for无法使用
9、指针空值nullptr
先来看这段代码:
cpp
#include <iostream>
using namespace std;
void test(int)
{
cout << "void test(int)" << endl;
}
void test(int*)
{
cout << "void test(int*)" << endl;
}
int main()
{
test(0);
test(NULL);
test(nullptr);
return 0;
}
可以看到0和NULL都是调用参数为int类型的函数,而nullptr是int*类型的函数 NULL本质上是一个宏,可能被定义为字面常量0或者是无类型指针(void*)0。 所以C++11引入了用nullptr表示指针空值,无需包含头文件。
至此有关C++入门的介绍结束,下一篇是类和对象。 如有问题欢迎指正。