什么是c++
C语言是结构化和模块化的语言,适合处理较小规模的程序。对于复杂的问题,规模较大的程序,需要高度
的抽象和建模时,C语言则不合适。为了解决软件危机, 20世纪80年代, 计算机界提出了OOP(object
oriented programming:面向对象)思想,支持面向对象的程序设计语言应运而生。
1982年,Bjarne Stroustrup博士在C语言的基础上引入并扩充了面向对象的概念,发明了一种新的程序语
言。为了表达该语言与C语言的渊源关系,命名为C++。因此:C++是基于C语言而产生的,它既可以进行C语
言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行面向对象的程
序设计。
一、c++关键字
在这里我们可以先了解一下
二、c++的命名空间
或许很多人都知道在编写一个cpp程序时要加入 using namespace std ;
但你知道这究竟是为什么嘛?
———在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
//命名空间的定义
//1. 定义命名空间首先用关键字namespace,在关键字后面加上名称,再加一对{}即可
namespace n1 //n1 为命名空间的名称
{
int a = 0; //命名空间中可以定义函数,也可定以变量
int add(int a, int b)
{
return a + b;
}
}
//2. 命名空间可以嵌套
namespace n2
{
namespace n3
{
}
}
//3. 同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。
namespace n1
{
int Mul(int left, int right)
{
return left * right;
}
}
注意:一个命名空间定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间当中
命名空间的使用
#include <iostream>
namespace N
{
int a = 10;
int b = 20;
int add(int left, int right)
{
return left + right;
}
int sub(int left, int right)
{
return left - right;
}
}
//定义了如上的空间N1 ,使用它一共有三种方式
int main()
{
//printf("%d", a);//直接使用编译器会报错
}
//1.0 加命名空间名称及作用域限定符
int main()
{
printf("%d ", N::a);
printf("%d ", N::b);
}
//2.0 使用using将命名空间中成员引入
using N::b;
int main()
{
printf("%d ", N::a);
printf("%d ", b);
}
// 3.0 使用using namespace 命名空间名称引入
using namespace N;
int main()
{
printf("%d ", a);
printf("%d ", b);
int c=add(1, 2);
printf("%d ", c);
int d=sub(4, 3);
printf("%d ", d);
}
c++中的输入和输出
//c++中如何实现hello world
#include <iostream>
using namespace std;
int main()
{
cout << "hello,world" << endl;
return 0;
}
相比c语言来说我们发现c++的输出方式更为简单
说明:
- 使用cout标准输出(控制台)和cin标准输入(键盘)时,必须包含< iostream >头文件以及std标准命名空
间。
注意:早期标准库将所有功能在全局域中实现,声明在.h后缀的头文件中,使用时只需包含对应头文件
即可,后来将其实现在std命名空间下,为了和C头文件区分,也为了正确使用命名空间,规定C++头文
件不带.h;旧编译器(vc 6.0)中还支持<iostream.h>格式,后续编译器已不支持,因此推荐使用
+std的方式 - 使用C++输入输出更方便,不需增加数据格式控制,比如:整形–%d,字符–%c
#include <iostream>
using namespace std;
int main()
{
int a;
double b;
char c;
cin >> a;
cin >> b >> c;
cout << a << " " << b << " " << c << endl;
return 0;
}
运行结果如下
想必到达这里就会明白为什么要用using namespace std;这一串话了(这段话告诉编译器我们要用名字空间std中的函数或者对象)
但是日后当我们在写成熟的代码的时候,一般不建议将标准命名空间全部打开,而是需要用库里的什么就打开什么。这就有效的防止了命名冲突
例如写一个hello world
第一种就是上面提到的
不安全但是简单
#include <iostream>
using namespace std;
int main()
{
cout << "hello,world" << endl;
return 0;
}
第二种,用哪个,提前打开那个对象
安全,推荐使用
#include <iostream>
using std::cout;
using std::endl;
int mian()
{
cout << "hello world" << endl;
}
第三种,什么时候用什么时候打开std
安全,但是较为复杂
#include <iostream>
int main()
{
std::cout << "hello world" << std::endl;
}
三、 缺省参数
缺省参数的概念:
缺省参数是声明或定义函数时为函数的参数指定一个默认值。在调用该函数时,如果没有指定实参则采用该默认值,否则使用指定的实参。
缺省参数又分为全缺省与半缺省
1.0全缺省
#include<iostream>
using namespace std;
void fun (int a = 10, int b = 20, int c = 30)
{
cout << "a= " << a << endl;
cout << "b= " << b << endl;
cout << "c= " << c << endl<<endl;
}
int main()
{
fun();// 不传参就用默认的
fun(1,2,3); //传了就用传了的
fun(1);
fun(1, 2);
}
运行结果如下
2.0 半缺省参数
#include<iostream>
using namespace std;
void fun2 (int a , int b , int c=30 )
{
cout << "a= " << a << endl;
cout << "b= " << b << endl;
cout << "c= " << c << endl<<endl;
}
int main()
{
fun2(1,2); // 没传就用默认值
fun2(1, 2, 3);
}
但要注意:
- 半缺省参数必须从右往左依次来给出,不能间隔着给
eg:
这三种写法都是错误的 - 缺省参数不能在函数声明和定义中同时出现
eg:
//a.h
void TestFunc(int a = 10);
// a.c
void TestFunc(int a = 20)
{}
// 注意:如果生命与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那
个缺省值
3.缺省值必须是常量或者全局变量
4.C语言不支持(编译器不支持)
缺省函数的作用就是为了是函数调用更加灵活
四、函数重载
函数重载的概念::是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的
形参列表(参数个数 或 类型 或 顺序)必须不同,常用来处理实现功能类似数据类型不同的问题
#include<iostream>
using namespace std;
int Add(int left, int right)
{
return left + right;
}
char Add(char left,char right)
{
return left + right;
}
double Add(double left, double right)
{
return left + right;
}
long Add(long left, long right)
{
return left + right;
}
int main()
{
cout<<Add(10, 20)<<endl;
cout<<Add(10.0, 20.0)<<endl;
cout<<Add(10L, 20L)<<endl;
cout<<Add('1', '2')<<endl;
return 0;
}
结果如下
注意:这两个函数就不构成运算符重载,因为参数的类型,个数都一样
short Add(short left, short right)
{
return left+right;
}
int Add(short left, short right)
{
return left+right;
}
下面两个函数构成重载吗?
void TestFunc(int a = 10) {
cout<<"void TestFunc(int)"<<endl; }
void TestFunc(int a) {
cout<<"void TestFunc(int)"<<endl; }
答案:不构成,他俩类型相同,只是缺省参数不同`
学习了函数重载,我们不禁联想到为什么c++支持函数重载,而C语言不支持函数重载呢?
在这里仅简单提一下,简单来说是因为C语言没办法支持重载,因为同名函数没办法区分。而C++是通过函数修饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。
**
extern “C”
**
有时候在C++工程中可能需要将某些函数按照C的风格来编译,在函数前加extern “C”,意思是告诉编译器,将该函数按照C语言规则来编译。比如:tcmalloc是google用C++实现的一个项目,他提供tcmallc()和tcfree两个接口来使用,但如果是C项目就没办法使用,那么他就使用extern “C”来解决。
extern "C" int Add(int left, int right);
int main()
{
Add(1,2);
return 0;
}
五、引用
引用的概念:引用不是定义一个新的变量,而是给已知的变量取一个别名,编译器不会为引用变量开辟内存空间,它和它引用的的变量共用同一块内存空间。 (通俗来说,引用就是取别名)
就比如:比如:李逵,在家称为"铁牛",江湖上人称"黑旋风”。
int main()
{
int a = 10;
//&在类型后面,就是引用的意思
int& b = a;// b就是a的引用(别名)
b = 20;
int& c = b;//c就是b的引用
c = 30;
}
通过监视我们可以看出完全符合引用的概念。
引用的作用
1.0 我们最熟悉的也就是交换两个值
void swap(int* a, int* b) //C语言中的交换需要指针(形参的改变不影响实参的改变)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
void swap(int& a, int& b)
{
int r = a;
a = b;
b = r;
}
int main()
{
int a = 10;
int b = 20;
swap(a,b);
swap(&a, &b);
}
- 引用特性
- 引用在定义时必须初始化
- 一个变量可以有多个引用
- 引用一旦引用一个实体,再不能引用其他实体
int main()
{
//一个变量可以有多个别名
int a = 0;
int& b = a;
int& c = b;
int& d = c;
int &x=a;
int r = 10;
x = r; //将r的值赋给x;
}
常引用
eg1:
int main()
{
const int a = 10;//const修饰代表a不能修改
//int& ra = a;// 错误 ra引用a属于权限的放大,所以不行
const int& ra = a;//正确
int b = 10;
int& rb = b;
const int& crb = b;//正确,crb引用b属于权限的缩小,所以可以
//b是可读可写的,crb是可读的,权限缩小,所以可行
}
eg2:
```cpp
int main()
{
int c = 10;
double d = 1.11;
d = c;
}
上述转换是可以的,发生了隐式类型转换,首先产生一个中间临时变量,这个中间临时变量是double类型的,将c赋给临时变量,再将临时变量赋给d
int main()
{
int c=10;
double& rc=c; //错误写法,编译通不过
const double &rc=c;//正确写法
}
后者正确是因为:发生类型赋值过程当中,将int给double,中间就会产生临时变量,临时变量是double类型,所以rc引用的是这个临时变量,并且这个临时变量具有常性(只是可读的),所以加了const后编译通过,类似于eg1中的第一个情况。
使用场景
1.0 做参数
void Swap(int& left, int& right) {
int temp = left;
left = right;
right =templ;
}
2.0 做返回值
在这里首先简单介绍下传值返回(见下图)
(1)传值返回时,调用Add完成后会产生int类型的临时变量,c给了临时变量,临时变量再给给ret
返回的对象是c的拷贝
通过上图我们可以证明确实产生了临时变量(ret引用的不是c,其实是这个临时变量,临时变量具有常性,所以第二个引用编过了)
(2)传引用返回
传引用返回 ,返回的返回对象是c的引用(别名)
int &Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int ret = Add(1, 2);
return 0;
}
主函数中并没有加const编译通过,所以证明返回的是c的引用
这段代码看着是没问题,编译器也通过了,但它实际上却存在这问题,ret的值是不确定的
解释如下,调用函数就会开辟栈帧
接下来再看这段代码‘
int &Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int &ret = Add(1, 2);
Add(5, 7);
cout << ret << endl;
return 0;
}
解释如下:
int &Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int &ret = Add(1, 2);
Add(5, 7);
printf("hello\n");
cout << ret << endl;
return 0;
}
这里ret是随机值原理和上述还是一样,调用printf时c那块空间就是随机值
注意:一个函数调用就会向下空间建立一个栈帧,函数调用结束,栈帧就会销毁
实际过程中,出了函数作用域,返回对象就不存在了,不能用引用返回
加了static就可正常使用了
int &Add(int a, int b)
{
static int c = a + b;
return c;
}
int main()
{
int &ret = Add(1, 2);
Add(5, 7);
printf("hello\n");
cout << ret << endl;
return 0;
}
ret的值也不会改变。
小小总结下:如果函数返回时,出了函数作用域,如果返回对象还未还给系统(也就是没销毁,例如加了static),则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。
**
传参和传引用效率
当参数和返回值是比较大的变量时,传引用传参和传引用做返回值可以提高效率,只要符合条件,尽量用引用传参,传返回值
2.引用与指针的区别
int main()
{
int a = 10;
//在语法上,这里给a这块空间取了一块别名,没有开辟新空间
int& ra = a;
ra = 20;
//在语法上,这里定义个指针变量,开辟了4个字节,存储a的地址
int* pa = &a;
*pa = 20;
}
但从汇编来看,引用的底层也是类似指针存地址的方式进行的
引用和指针的不同点:
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
- 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全
六、内联函数
c++频繁调用的函数,定义成inline,会在调用的地方展开,没有栈帧的开销
inline int add(int x,int y) //直接在函数前加一个inline构成inline函数
{
return x + y;
}
int main()
{
int c = 0;
}
c语言为了小函数避免建立栈帧的消耗-》》提供了宏函数支持,预处理阶段展开
既然c语言已经解决了,为什么c++还要提供inline函数呢?(宏函数的缺点)
a.不支持调试 b.宏函数语法复杂容易出错 c.没有类型安全的检查
例如写一个add的宏函数
#define Add(int x,int y) return x+y;//典型的错误写法
#define Add(x,y) x+y; //错误 分号问题
#define Add(x,y) (x)+(y) // 错误 优先级问题
//标准写法
#define Add(x,y) ((x)+(y))
内联函数特性
- inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环/递归的函数不适宜使用作为内联函数。
- inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等等,编译器优化时会忽略掉内联。
- inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
七、auto关键字
int a = 10;
auto b = a;//类型声明成auto,可以根据a的类型自动推到b的类型
int main()
{
int x = 10;
auto a = &x;
auto* b = &x;
int& y = x;
auto c = y;
auto& d = x; //指定了d是x的引用
//打印变量的类型
cout << typeid(x).name() << endl;
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(y).name() << endl;
cout << typeid(c).name() << endl;
}
2.0 在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
void TestAuto()
{
auto a = 1, b = 2;
auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}
auto不能推到的场景
1.auto不能作为参数的函数
// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{}
2.auto不能用来推导数组
void TestAuto()
{
int a[] = {1,2,3};
auto b[] ={4,5,6};
}
八、 基于范围的for循环
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
int main()
{
int arr[] = { 1,2,3,4,5 };
for (int i = 0;i < sizeof(arr) / sizeof(arr[0]);i++) //常规方法
{
cout << arr[i] << " ";
}
cout << endl;
//范围for c++11 新语法遍历,更简单
//自动遍历,依次取出arr中的元素,赋值给e,直至结束(e可以任意取其他名称)
for (auto e : arr)
{
cout << e << " ";
}
cout << endl;
return 0;
}
但如果我们想要改变数组的内容我们就需要借助引用
见如下代码
#include<iostream>
using namespace std;
int main()
{
int arr[] = { 1,2,3,4,5,6 };
//自动遍历,依次取出arr中的元素,赋值给e,直至结束
for (auto e : arr)
{
e *= 2;
}
for (auto ee : arr)
{
cout << ee<<" ";
}
return 0;
}
是不是以为会输出2 4 6 8 10 12,但这个范围for 是自动遍历,依次取出arr中的元素,赋值给e,直至结束,arr是不会改变的如果想要改变增加引用即可
#include<iostream>
using namespace std;
int main()
{
int arr[] = { 1,2,3,4,5,6 };
//自动遍历,依次取出arr中的元素,赋值给e,直至结束
for (auto& e : arr)
{
e *= 2;
}
for (auto ee : arr)
{
cout << ee<<" ";
}
return 0;
}
这样就达到了我们的需求,e是arr每个成员的别名,e改变arr成员也改变。
九、指针空值nullptr(C++11)
NULL实际是一个宏,在传统的C头文件(stdio.h)中,可以看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如:
void f(int a)
{
cout<<"f(int)"<<endl;
}
void f(int*a)
{
cout<<"f(int*)"<<endl;
}
int main()
{
f(0);
f(NULL);
f(nullptr);
return 0;
}
程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的初衷相悖,所以引入nullptr来解决这一问题。
**注意:
- 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
- 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
- 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。**