C++是基于C语言而产生的,它既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行面向对象的程序设计。
其中画圈的是C语言的关键字。这里要注意了:false和true并不是C语言的关键字。
所以说:C++兼容C的绝大多数语言特性
在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,
namespace
关键字的出现就是针对这种问题的。
#include
#include //这个头文件中包含 rand这个库函数
int rand = 0;//定义的rand这个变量,与库函数中的rand函数重名,所以命名冲突了
int main()
{
printf("%d\n",rand);
}
//这时如果进行编译,则会报错
//编译报错:error C2365: “rand”: 重定义;以前的定义是“函数”
// 命名冲突问题
// 1、我们自己定义的变量、函数可能跟库里面重名冲突
// 2、进入公司项目组以后,做的项目通常比较大。多人协作,两个同事写的代码,命名冲突。
// C语言没有办法很好的解决这个问题
// CPP提出一个新语法:命名空间--关键字namespace
定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字(随自己定义),然后接一对{} 即可,{}中即为命名空间的成员。
注意:一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中
例如:将上方代码进行修正,如下:
#include
#include
//定义了一个叫xnh的命名空间 -- 命名空间定义的是一个:域
namespace xnh
{
int rand = 0;
}
int main()
{
printf("%d\n", rand);//访问的是中的rand函数,打印的是以十进制打印的该函数地址
printf("%d\n", xnh::rand);//访问的是xnh这个命名空间中的rand变量,打印显示为0
return 0;
}
在上述代码中会发现,若想访问命名空间中的变量,则需要借助一个作用符 :: 这个符号叫做 域作用限定符,xnh :: rand 的意思就是,去左边这个叫xnh的域(命名空间)里面找rand这个变量。
请注意:命名空间内的变量只能允许声明和初始化,而不能在其中进行赋值!
namespace xnh
{
int a;//ok
int b=10;//ok
//b=20;//no
}
namespace xnh
{
// 1、命名空间中可以定义变量/函数/类型
int rand = 10;
int Add(int left, int right)
{
return left + right;
}
struct Node
{
struct Node* next;
int val;
};
}
//使用
int main()
{
xnh::rand = 20;
struct xnh::Node node;//注意结构体与函数和变量的不同
xnh::Add(1, 2);
return 0;
}
//2. 命名空间可以嵌套定义
namespace N1
{
int a;
int b;
int Add(int left, int right)
{
return left + right;
}
namespace N2
{
int c;
int d;
int Sub(int left, int right)
{
return left - right;
}
}
}
//使用
int main()
{
N1::a = 1;
N1::N2::c = 2;
N1::Add(1, 2);
N1::N2::Sub(3, 4);
}
我们在一个工程中,我们可以将函数声明和定义分开来写,如下:
//在List.h头文件中只写声明
namespace xnh
{
int rand;
struct ListNode
{
//...
};
void ListInit();
void ListPushBack(struct ListNode* phead, int x);
}
//在List.cpp源文件中写定义
namespace xnh
{
void ListInit()
{
// ...
}
void ListPushBack(struct ListNode* phead, int x)
{
//...
}
}
//在test.cpp源文件中使用
int main()
{
struct xnh::ListNode ln;
xnh::ListPushBack(NULL, 1);
return 0;
}
虽然将xnh
这个命名空间分开写在了List.h
和List.cpp
两个文件中,但最后会合成同一个命名空间中。
using namespace 命名空间名称;
这句代码的意思就是把整个命名空间展开,这样当我们使用命名空间下的变量、函数等等就不需要加作用域限定符了,用起来方便,但隔离失效了。
这样容易造成命名冲突问题,为了解决这个问题,出现了第三种引入方法。
第三种方法就是指定展开–把常用的展开,自己在定义的时候避免跟常用重名即可
例如:
这种方法可以防止命名冲突的问题,因为它只引入了一部分。
cout :输出
cin :输入
我们初学一门语言,输出Hello world是必不可少的,C++输出方法如下:
#include
using namespace std;
int main()
{
cout<<"hello world!"<<endl;
return 0;
}
对以上程序有几点解释:
首先是iostream头文件,是标准输入/输出文件,可以支持使用cout和cin等函数,分别代替printf和scanf;
其次是using namespace std,就是我们上文讲到的命名空间,std是C++标准库的命名空间名,C++将标准库的定义实现都放到这个命名空间中
还有就是c++特有的输入输出,cout,cin,都有与之对应的符号,如下:
<< :流插入操作符
。与cout配合使用,可以将所有你需要输出的变量或者字符串流入到cout中,让cout负责输出。我们将cout相当于控制台就好理解了
>> :流提取操作符
。与cin配合使用,可以将你输入的值流入到某变量中。
就相当于把你在屏幕上输入的数据或字符串通过>>符号流入到所需的某变量中。我们可以将cin相当于键盘
还有就是 endl与"\n"(换行符)等价,都是进行换行的意思。
注意:使用 cout标准输出(控制台) 和 cin标准输入(键盘) 时,必须包含< iostream >头文件以及std标准命名空间。
cout
:可以自动识别变量的类型
使用示例:
int a = 10;
char arr[] = "abcdef";
double b = 1.11;
float c = 0.0;
cin>>c;
cout<<a<<endl;
cout<<arr<<endl;
cout<<c<<endl;
缺省参数是声明或定义函数时为函数的参数指定一个默认值。在调用该函数时,如果没有指定实参则采用该默认值,否则使用指定的实参。
//缺省参数
#include
using namespace std;
//这儿的 0 就相当于缺省参数,如果实参什么都没传过来,缺省参数就赋值给a。
void func(int a = 0)
{
cout << a << endl;
}
int main()
{
//调用
func(10);
func(); //在c语言中这样写肯定是不行的,但是在c++中有了缺省参数,如果你什么都不传,只要你前面有缺省参数的存在,就能过。
return 0;
}
全缺省参数就是为函数的所有参数都设置一个默认参数,例如:
void Func(int a = 10, int b = 20, int c = 30)
{
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
我们调用此函数时,有以下几种方法:
Func();
Func(1);
Func(1,2);
Func(1,2,3);
传进去的实参必须是从左向右传的,而且必须连续的。比如以下传参方法就是错误的
Func(,10,);//错误,传参必须是从左往右,连续传参
半缺省参数就是将函数的参数部分初始化,例如:
void Func(int a, int b = 10, int c = 20)
{
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
注意:
1.半缺省参数必须只能从右向左依次给出,而且必须连续,不能间隔着给。
例如以下方式就是不行的
int Func(int a,int b=10,int c);//错误,不连续
int Func(int a=10,int b=20,int c);//错误,不是从右往左给出
2.缺省参数不能在函数声明和定义中同时出现,推荐在声明中给
//a.h
void Func(int a = 10);
// a.c
void Func(int a = 20)
{
//.....
}
// 注意:如果声明与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那个缺省值。
3.缺省值必须是常量或者全局变量
4. C语言不支持(编译器不支持)
重载的意思是具有多重含义,那么函数重载即是一个函数具有多种功能,也就是对同一个函数名的不同的“解释”。
定义:
C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 顺序)必须不同,即参数个数,参数顺序,参数类型三者有一个不同即可。常用来处理实现功能类似数据类型不同的问题
比如:
//参数类型不同
int Add(int a, int b)
{
return a+b;
}
double Add(double a, double b)
{
return a+b;
}
//参数个数不同
int Func(int a);
int Func(int a,int b);
//参数顺序不同 -- 顺序不同指的是,形参类型的顺序不同
int Sub(int a, char b);
int Sub(char b, int a);
注意:函数重载与函数返回值类型无关。
为什么C++支持函数重载而C语言却不支持呢?
在C/C++中,一个程序要运行起来,C/C++的源文件都是需要进行预处理,编译,汇编,链接,最后生成可执行程序的。和C的源文件一样,都是源文件先单独编译生成目标文件合到一起链接成可执行程序。
实际我们的项目通常是由多个头文件和多个源文件构成,函数的声明和定义分离会使编译器在编译源文件时暂时找不到函数的地址,那么其查找函数的规则则会在链接时体现。所以将函数的声明和实现放到两个文件中,更方便观察C++和C对于函数名字修饰规则的不同。
由于Windows下vs的修饰规则过于复杂,而Linux下gcc的修饰规则简单易懂,下面我们使用了gcc演示了这个修饰后的名字。
结论:在linux下,采用gcc编译完成后,函数名字的修饰没有发生改变。
结论:在linux下,采用g++编译完成后,函数名字的修饰发生改变,编译器将函数参数类型信息添加到修改后的名字中。
通过这里就可以理解C语言无法支持重载了,因为c++会根据函数的参数对函数名进行修饰,只要参数不同,修饰出来的名字就不一样,就支持了重载,而C对函数名却不会修饰,所以没办法支持重载,因为同名函数没办法区分。
函数名不相同,其生成的函数地址也不会相同,所以在调用的时候也不会产生冲突
我们看完 函数重载 的定义后,可能会产生一个疑问,如:
返回值不同,构成重载吗??为什么呢??
请看下面调用函数:
//两者返回值不同
int f(int a, int b)
{
cout << "f(int a,char b)" << endl;
return 0;
}
char f(int a, int b)
{
cout << "f(int a,char b)" << endl;
return 'A';
}
int main()
{
f(1, 1);
f(2, 2);
//这里调用会存在二义性,因为调用时不指定返回类型,所以他不知道该调用哪个函数
return 0;
}
返回值不同,不构成重载原因,并不是函数名修饰规则
真正原因是调用时的二义性,无法区分,
因为调用时不指定返回值类型
引用是C++中一个重要的语法,在后期应用非常广泛。相较于指针,它更加方便也更好理解。
定义:
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
通俗来讲,就是为一个变量取一个别名。就如同家人为你取一个小名,两个名字都代表着同一个人。
引用的基本使用:
类型+& +引用变量名(对象名) = 引用实体;
void func()
{
int a = 10;
int& b = a;//为a取别名叫b
b = 20;
int& c = b; //为b取别名叫c
c = 30;
}
运行结果:
从这里可以看出这里分别给a取了两个别名,并且别名值的改变也会影响变量a,因为别名本身代表的就是a,同时,这三个变量的地址都是同一地址,所以证明引用实体和引用变量共用同一块内存空间。
void func()
{
int a = 10;
//int& b; //err,定义引用必须初始化
int &b = a;//正确处理
}
void func()
{
int a = 10;
int &b = a;//正确处理
int &c = a;
int &d = a;
}
int a = 10;
int& b = a;
int c = 20;
b = c;//这里是把c的值赋给b,并不是让b变为c的别名
常引用的定义:被const修饰的引用就是常引用。
常引用会涉及到权限的问题,如:
//权限放大
const int a = 0;
int& ra = a;//Err
//权限缩小
int b = 0;
const int& rb = b;
//权限相同
const int c = 0;
const int& rc = c;
引用const修饰的常变量时,如果引用不加const,那么就造成了权限扩大显然时不允许的。权限不允许扩大但是可以不变和缩小,在用引用做参数时,可以加const修饰防止实参被修改。
double d = 1.11;
int i = d;
double d = 1.11;
const int& i = d;
将浮点型变量d赋值给整型变量i时会发生隐式类型转换,中间会产生一个临时变量,将d的数据截断放入临时变量中再将临时变量赋值给i。类似于不同整型数据进行比较会发生整型提升,其实就是将各自产生的临时变量进行整型提升再进行比较,这也是二者的值不会发生改变的原因。
这样的临时变量是个右值,具有常属性,所以只有常引用才能引用这样的临时变量。也就使得const Type&的引用类型可以引用任意类型的变量。
void Swap(int& rx, int& ry)
{ //传引用
int tmp = rx;
rx = ry;
ry = tmp;
}
int main()
{
int x = 0, y = 1;
Swap(x, y);
cout << x << " " << y << endl;
return 0;
}
但引用做参数时,在一些情况下会产生歧义,例如:
void Swap(int x, int y)
{ //传值
int tmp = x;
x = y;
y = tmp;
}
void Swap(int* px, int* py)
{ //传址
int tmp = *px;
*px = *py;
*py = tmp;
}
void Swap(int& rx, int& ry)
{ //传引用
int tmp = rx;
rx = ry;
ry = tmp;
}
int main()
{
int x = 0, y = 1;
Swap(&x, &y);//这里调用没问题,是进行传址调用
cout << x << " " << y << endl;
Swap(x, y);//err,这里就会产生错误,编译器将不会知道是调用传值还是传引用,所以会产生歧义
cout << x << " " << y << endl;
return 0;
}
三者参数列表分别为整型、指针类型和引用类型,虽构成了函数重载,但调用时仍会有歧义。
引用做参数举例:
如图所示,单链表中因可能修改头指针plist而使用二级指针传参的方法可以选择一级指针的引用替代。phead就是plist的引用,改变phead就是改变plist。
传引用返回:这点比较难理解,先看下面的例子:
// 传引用返回
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
cout << ret << endl;
return 0;
}
当Add函数栈帧销毁后,函数返回了c的引用,再执行int& ret=Add(1,2)相当于取c的值赋值给ret,而c的内存空间已返还给操作系统,就造成了非法访问。
mov eax, dword ptr [c]
//将变量c的值放入寄存器eax中
lea eax, [c]
//将变量c的地址放入寄存器eax中
返回值和返回引用的区别就是一个用临时变量存储了c的值返回的是拷贝对象,一个用引用访问了原来c的内存空间读取c的值返回的是返回值对象本身。之所以将地址放到寄存器中带回,是因为引用的底层实现是指针,一会要通过地址访问c的内存空间。
例如:
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
cout << ret << endl;
Add(10, 20);
cout << ret << endl;
return 0;
}
两次打印ret一次是3一次是30。从这个例子可以看出,引用将c的地址带了回来ret也初始化成为c的引用,Add栈帧销毁又创建,每次的c变量都在同一块空间ret也引用了这块空间,所以下次调用Add函数时这块空间的值被修改成了30。
但这输出的30是唯一的值吗?答案是错误的。
需要注意的是这个ret的结果可能会是随机值,因为c是一个局部变量,随着栈帧的销毁会一块销毁,当空间被销毁了,这块空间就会归还给操作系统,程序员就没有使用权限了,而为什么是随机值,是因为这块空间可能会被清空置成随机值,当我们在打印ret结果值前面加一个printf时,ret就会变为随机值
总结:
出了函数作用域,返回变量不存在了,不能用引用返回 ,因为引用返回的结果是未定义的
出了函数作用域,返回变量存在,才能用引用返回。
传值与传引用效率对比
#include
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;
}
传引用和传指针差不多,每次调用都访问的是同一块空间,而传值每次调用都会开辟一样大的空间,所以传引用的效率比传值高很多,数据越大对性能的提升越大。引用可以作输出型参数或输出型返回值,就是达到形参改变外面的实参的目的。
语法层:
指针和引用是完全不同的概念,指针是开辟空间,存储变量的地址,引用不用开辟空间,仅仅是对变量取别名。
底层汇编时:
从上图汇编代码可以看出,引用是用指针实现的。
在频繁调用一些小的,简单,仅有几行代码的函数时,我们就会反复的开辟栈空间,然后进行函数压栈,销毁等一系列操作,这样不仅会浪费空间,还降低效率。解决这种问题除了使用C语言的语法宏以外,C++还有新语法叫内联函数。
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率。
如果在上述函数前增加inline关键字将其改成内联函数,在编译期间编译器会用函数体替换函数的调用。
这时就没有调用ADD函数,而是运行到此语句时,将函数进行了展开,这样就提升了程序的效率。
宏的优点是可以增强代码的复用性,提高性能,缺点是无法调试,可维护性差,没有类型检查。C++出台内联函数就是推荐使用内联函数。
auto是c++11引进的关键字.
C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型
int TestAuto()
{
return 10;
}
int main()
{
int a = 10;
auto b = a;
auto c = 'a';
auto d = TestAuto();
//typeid(变量).name 查看变量类型
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
//auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化
return 0;
}
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
int main()
{
int x = 10;
auto a = &x; //用auto和auto*没有任何区别
auto* b = &x; //用auto和auto*没有任何区别
auto& c = x; //用auto声明引用类型时则必须加&
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
*a = 20;
*b = 30;
c = 40;
return 0;
}
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
void TestAuto()
{
auto a = 1, b = 2;
auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}
// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)//err
{}
void TestAuto()
{
int a[] = {1,2,3};
auto b[] = {4,5,6};//err
}
为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有lambda表达式等进行配合使用。
对于一个有范围的集合仍需说明它的范围,这无疑是多余的,因此C++11引入范围for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
:前是循环变量,后面是循环范围
//C
for (int i = 0; i < sz; i++)
{
cout << arr[i] << endl;
}
//C++
for (auto e : arr)
{
cout << e << endl;
}
注意:与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环。
范围 for 循环返回的对象是数组元素值的拷贝,所以若要写入数组元素的话,需要使用引用。
for (int i = 0; i < sz; i++)
{
arr[i] *= 2;
}
for (auto& e : arr)
{
e *= 2;
}
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
void TestFor(int arr[])
{
for (auto e : arr)
{
cout << e << endl;
}
}
数组传参本质就是指针,所以不知道数组的具体范围,因此是错误的。
在良好的C/C++编程习惯中,在声明一个变量的同时最好给该变量一个合适的初始值,否则可能会出现不可预料的错误。比如未初始化的指针,如果一个指针没有合法的指向,我们基本都是按如下方式对其进行初始化:
int* p1 = NULL;
int* p2 = 0;
NULL其实是一个宏,在传统的C头文件(stddef.h)中可以看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如:
#include
using namespace std;
void Fun(int p)
{
cout << "Fun(int)" << endl;
}
void Fun(int* p)
{
cout << "Fun(int*)" << endl;
}
int main()
{
Fun(0); //打印结果为 Fun(int)
Fun(NULL); //打印结果为 Fun(int)
Fun((int*)NULL); //打印结果为 Fun(int*)
return 0;
}
程序本意本意是想通过Fun(NULL)调用指针版本的Fun(int* p)函数,但是由于NULL被定义为0,Fun(NULL)最终调用的是Fun(int p)函数。
注:在C++98中字面常量0,既可以是一个整型数字,也可以是无类型的指针(void*)常量,但编译器默认情况下将其看成是一个整型常量,如果要将其按照指针方式来使用,必须对其进行强制转换。
对于C++98中的问题,C++11引入了关键字
nullptr
。
- 在使用
nullptr
表示指针空值时,不需要包含头文件,因为nullptr是C++11作为关键字引入的。- 在C++11中,sizeof(nullptr)与sizeof((void*)0)所占的字节数相同,大小都为4
- 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。