目录
C语言 && C++语言 ?
许多编程初学者大都是从C语言开始接触编程的,看到C++与C语言可能会傻傻分不清,以为这两者是同一种语言的不同标准之类。其实不然,C++的别称是cplusplus,它是C语言的进阶版,与C语言是完全不同的编程语言。
C语言
我们熟知的C语言是1972年大佬丹尼斯·里奇(Dennis MacAlistair Ritchie)根据B语言发展而来(C语言之前也有A、B语言,但是并不成熟)。C语言本质上是一门面向过程(以函数驱动)的语言,由于其简洁的语言风格,完善的结构化体系,高效的运行效率,一经问世就广为流传,一度成为应用最广泛的一门编程语言,大名鼎鼎的UNIX底层就是C语言。
由于C语言的效率高、直接面向计算机等特性使得该语言直至现在仍作为大多操作系统的底层语言。但并不是说C语言没有缺点:C语言是结构化和模块化的语言,适合处理较小规模的程序。对于复杂的问题,规模较大的程序,需要高度的抽象和建模时,C语言则不合适,这就是C++语言出现的原因。
C++
1983年由大佬本贾尼·斯特劳斯特卢普在C语言的基础上,引入面向对象的思想,研发出的一门面向对象的编程语言;C++与C语言完全兼容,C语言的绝大部分内容可以直接用于C++的程序设计,但它同时又有着面向对象语言的三大特性:封装、继承、多态。
C++发展的关键时期
- C++98:C++标准的第一个版本,绝大多数编译器都支持,得到了国际标准化组织(ISO)和美国标准化协会认可,以模板方式重写C++标准库,引入了STL(标准模板库)
- C++11:增加了许多特性,是最近对C++的较大的一次更新
(加入了正则表达式、基于范围for循环、auto关键字、新容器、列表初始化、标准线程库…) - 注:编程语言的版本并非越新越好,一些旧版本可能体系更加完善。
了解一个新事物最好的方式就是通过熟知概念开始,对于C++语言的认识,从它与C语言的对比开始最好不过。
对比C语言了解C++基础概念
1. 关键字
- 在C语言的C89(C90)版本中,一共有32个关键字。
- 在C++中我们只关注C++98版本,该版本共计63个关键字,其中也包含了C语言的32个关键字:
auto关键字(C++11)
auto这个关键字其实在C语言中就存在,其功能是:使用auto修饰的变量,是具有自动存储的局部变量;
但是编译器本身就支持这种功能,因此auto的用处并不大;
在C++11标准中,将auto的功能做了重定义:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得
注意
-
auto 定义变量必须有初始化,否则无法推导变量类型,编译失败
auto c; //错误,编译器无法确定变量c的类型
-
auto并不是一种类型的声明,而是一个类型的占位符
-
用auto声明指针类型时,用auto和auto*没有任何区别;auto声明引用类型时则必须加&
-
同一个auto定义多个变量时,所有类型必须一样,否则会编译出错
auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
-
auto不能使用的场景:
不能作为函数的参数和返回值类型:
void TestAuto(auto a) {}
(因为编译器无法对a的实际类型进行推导)
不能直接用来声明数组:auto b[] = {4,5,6};
取消auto原来用法:为避免与C++98中的auto概念发生混淆,C++11只保留了auto作为类型指示符的用法;
为什么要有auto
- auto 功能与typedef类似,用来简化类型名;
- 但是typedef 与 指针类型一起处理时易出现缺陷;
auto最巧妙的使用场景:基于范围的for循环
例1://在原来的C语言用循环:
// 遍历输出一个数组的所有元素
void Test_for()
{
int arr[] = { 1, 2, 3, 4, 5, 6 };
// C语言中遍历一个已定义的数组,通过计算数组长度for循环遍历
int size = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < size; i++)
{
printf("%d ", arr[i]);
}
}
当需要遍历一个有范围的集合时,人工计算size大小确定循环范围其实是多余的,因为这个集合大小其实在编译器已经确定。
因此C++11中引入了基于范围的for循环,for循环的括号内由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
例2:
// 将一个数组*2并输出
void Test_for()
{
int arr[] = { 1, 2, 3, 4, 5, 6 };
/*
for (auto a : arr)
{
a *= 2;
}
*/
// 上面写法错误,auto a相当于int a = arr[i],arr的值并不受影响
// 若要影响原数组,必须采用指针或引用
for (auto& a : arr)
{
a *= 2;
}
// 输出操作不影响原数组,直接auto即可
for (auto b : arr)
{
cout << b << " ";
}
cout << endl;
}
范围for的使用条件:for循环迭代的范围必须是确定的
2. C++的命名空间
是什么?
在C++中命名空间用来表示一个标识符的可见范围,本质上就是一个具有名字的作用域
为什么?
- 要了解C++的命名空间,我们从C语言的作用域的角度来看:
- C语言通过变量(函数)的全局性或局部性来决定该变量的作用范围。但这种规则实际操作中会有不小的缺陷:
- 解决大型问题时,当项目合并遇到相同全局变量,重命名的全局变量会出现冲突
- C语言编程中,自定义的变量名易于库文件变量名称相同,从而产生冲突
基于上述缺陷,C++采用命名空间对标识符的名称进行本地化(明确作用域),避免命名冲突
怎么做?
使用关键字 namespace 即可定义命名空间
- 定义格式:
namespace 空间名{...}
- 空间成员:
- 自定义变量、
- 自定义函数、
- 自定义类型(结构体…)
- 使用特性:
- 就近使用原则
- 命名空间可以嵌套使用
- 在一个工程中,可以定义相同名称的命名空间,编译器会将同名命名空间合并为一个
- 使用场景:对于命名空间的使用,要使用作用域运算符:“ :: ”
- ::变量名 表明该变量是全局变量
- 命名空间::变量名/函数(最推荐的使用方式)
- using 空间名::变量名:当一个命名空间的某一变量频繁使用时,将该语句加与主函数之上,表示所有该变量均为此命名空间。
- using namespace 作用空间名:当一个命名空间所有变量频繁使用,将该语句加与主函数之上,表示之后的所有变量均出自次命名空间。
举例说明命名空间的使用(VS2013环境):
例1. 对比前三种方式
// 定义一个全局变量
int a = 0;
// 定义一个命名空间
namespace name1
{
int a = 10;
int Add(int left, int right)
{
return left + right;
}
}
// 定义一个命名空间
namespace name2
{
int b = 20;
}
//引用name2中的b变量
using name2::b;
int main()
{
// 1.::变量名:表明该变量是全局变量
printf("case1:%d\n", ::a);
// 2.命名空间::变量名/函数
printf("case2:%d\n", name1::a);
// 3.using 空间名::变量名
printf("case3:%d\n", b);
system("pause");
return 0;
}
结果:可以看到,虽然全局变量a与命名空间name1的变量a同名,但通过对::不一样的使用,编译器完全可以区分这种情况。
例2. 测试第四种方式
// 定义一个命名空间name3
namespace name3
{
int a = 30;
int b = 30;
}
//引用name3
using namespace name3;
int main()
{
// using namespace 作用空间名
printf("case3:%d\n", b);
system("pause");
return 0;
}
结果:可以看到using namespace 作用空间名这种方式只对
printf("case3:%d\n", b);
这种不带::符的变量有效
例3. 测试命名空间嵌套使用
// 定义一个命名空间name4
namespace name4
{
int c = 40;
namespace name5 //嵌套定义一个命名空间name5
{
int c = 50;
}
}
int main()
{
// 4.测试嵌套使用
printf("case4:%d\n", name4::c);
printf("case5:%d\n", name4::name5::c);
system("pause");
return 0;
}
结果:可以看到命名空间嵌套使用,仍不妨碍命名空间的独立性
3. 输入输出
C语言的输入输出
- C语言通过scanf & printf 这样的不定参数函数实现输入输出,其返回值是成功输入(出)数据的个数;
- 这种输入输出方式有些许不足之处:
输入输出需要增加数据格式控制;
输入时需取地址,易指向空指针造成内存问题。
C++的输入输出
C++本质上是利用IO流进行输入输出:
头文件
<iostream> 与 <stdio.h>
- 注意前者头文件没有.h;
<iostream.h>
是旧版本的形式<iostream>
流操作符头文件,包含了流运算符<< 与 >><stdio.h>
标准输入输出头文件(可以不带)[TIP]:stdio 就是指 “standard input & output"(标准输入输出)
形式
- 标准输出
std::cout << " " << std::endl
- ostream的流对象:cout = console out (控制台输出流)
- 流插入运算符 <<
- endl = end line(line指换行)
- std 指cout与end都在<stdio.h>这个头文件下,因此使用标准命名空间std
- 可以连续输出 std::cout<< " " << " "<< std::end
- 标准输入
std::cin >> 变量名
- istream的流对象:cin = console in (控制台输入流)
- 流提取运算符 >>
- 可连续输入 例:cin >> a >> b >> str
注意:
- cout、cin、endl的定义都包含在
<iostream>
头文件中; - 该种方式无需格式控制,能自动识别变量类型;
- 日常建议直接使用
using namespace std
或using std::cout
(一种偷懒); - 写大项目采用 std::cout之类(实现标准化);
- 若按进制输入输出,推荐使用C语言方式。
例:std::cout << std::hex << 100 << std::endl(16进制输出)
【Tip】
- 对于C++的输入输出操作,其实本质上也是一种函数,只不过函数并不是cin、cout;
- cin和cout是iostream类的2个对象;
- 输入输出的两个操作符<< 和 >>其实可以看作为一种函数(操作符重载时会详谈);
- 我们可以将这种操作符认为是函数
ostream& << (ostream& cout, 待输出的变量)
与istream& >> (istream& cin, 待输入至某变量)
;(其实不是,实际是在 ostream 类对 “<<” 进行了重载) - 可以重载的操作符还有很多,比如 ==、 -=、 +=、…后续详见其他博客;
- 可以看到 流插入运算符<< 的返回值是输出的数据流对象的引用;流提取运算符>> 的返回值是输入的数据流对象的引用;
4. C++的函数
C语言函数特性
- 函数无返回值时默认为int返回值;
- 若函数定义时为指明参数,调用时可传入多个参数。
C++函数特性
- 必须在函数定义时加上返回值 !!
- 若定义函数时没有参数,则调用函数不能传入任何参数。
缺省函数
为什么
在C语言中,函数的参数如果忘记传参,就无法实现函数功能;C++通过缺省函数这个功能,实现为函数参数提前设定默认值的功能。
是什么
- 缺省函数指C++中,在声明或定义函数时,可提前为参数提供一个默认值;
- 如果一个函数的参数是缺省参数,那么调用时可以不用传实参;
- 注意:缺省 == 默认
例1. 测试缺省参数功能
#include<iostream>
#include<Windows.h>
using namespace std;
void Test(int ret = 1)
{
cout << ret << endl;
}
int main()
{
cout << "传一个实参0:";
Test(0); //传参测试
cout << "不传参测试:";
Test(); //不传参测试
system("pause");
return 0;
}
分类:
- 全缺省参数:所有参数均带有默认值;
void Test1(int a = 1, int b = 2, int c = 3)
{
cout << "a:" << a << " b:" << b << " c:" << c << endl;
}
int main()
{
Test1();
system("pause");
return 0;
}
- 半缺省参数:部分参数带有默认值(规则:从右向左,不能间隔);
不能前面带后面不带。
void Test2(int a, int b = 2, int c = 3)
{
cout << "a:" << a << " b:" << b << " c:" << c << endl;
}
int main()
{
Test2(111, 222);
system("pause");
return 0;
}
注意:
- 一般缺省参数在函数声明中定义,但也可在函数定义中定义
- 缺省参数不能再函数定义和声明不能同时给出(重定义默认参数)
- 缺省值必须是常量或全局变量
- C语言并不支持缺省参数的概念
函数重载
概念
同一作用域中有几个功能类似的同名函数,他们的形参列表不同,这样的一组函数称为函数重载。
作用:C++引入该功能就可以处理功能类似数据类型不同的问题。
特点:两同一不同
- 函数作用域相同
- 函数名相同
- 参数不同(参数类型、次序、个数)
- 注意:与返回值是否相同无关!
例1:测试函数重载功能实现
#include<Windows.h>
#include<iostream>
int Add(int a, int b)
{
return a + b;
}
double Add(double a, double b)
{
return a + b;
}
using namespace std;
int main()
{
int Add_int = Add(1, 2);
double Add_double = Add(1.1, 2.2);
cout << Add_int << "\n" << Add_double << endl;
system("pause");
return 0;
}
原理
-
编译器在编译阶段,会根据传入的实参类型提前推演,匹配对应参数类型合适的函数;
-
若推演过程中未发现符合函数,编译器会尝试对参数的类型进行隐式类型转换,再寻找;例:Add(1.1,2.2) -> Add(1,2)
例2:测试该情况
#include<Windows.h>
#include<iostream>
int Add(int a, int b)
{
return a + b;
}
using namespace std;
int main()
{
int Add_int = Add(1, 2);
double Add_double = Add(1.1, 2.2);
cout << "参数为(1,2):"<<Add_int << endl;
cout << "参数为(1.1,2.2):" << Add_double << endl;
system("pause");
return 0;
}
- 当上述的隐式类型转换可以向多个方向转换,则直接报错;
例:Add(1.1,2)
例3:测试该情况
#include<Windows.h>
#include<iostream>
int Add(int a, int b)
{
return a + b;
}
double Add(double a, double b)
{
return a + b;
}
using namespace std;
int main()
{
int index = Add(1.1, 2);
cout << "参数为(1.1,2):" << index << endl;
system("pause");
return 0;
}
为什么C++可以支持函数重载而C语言不支持?
想弄清楚这个问题,首先要明白两个概念:
- C/C++的编译器工作方式:
- 名字修饰
是什么
- 在现代程序设计语言中,编译器需保证每个函数的实体名称唯一,而为每个函数名进行修饰;
- 编译器在链接时,当出现调用函数,就是通过修饰后的实体函数名来进行查找;
而不同语言的编译器修饰规则不同。
C语言的名字修饰
- 修饰规则:_ 函数名(VS);直接函数名(Linux)
- 可以看到C语言对函数名的修饰不涉及参数,因此C语言不支持函数重载
C++的名字修饰
- 修饰规则:涉及函数参数,不同厂商编译器的修饰规则不同
- visual C++编译器:
int func(int) -> ?func@@YAHH@Z
H表示Int,一个返 回值,后面都是参数- Linux g++编译器:
int func(int) -> _Z4funci
i表示int、4表示函数名占几个字符、没有返回值的表示
【解析】了解了上面的两个概念,我们再分析“为什么C++可以支持函数重载而C语言不支持”这个问题:
- 一个编程文件在运行的时候,需在编译器经过转化才能让计算机直接运行,而操作系统会在编译环节堆程序里的函数进程 名字修饰 以便识别查找;
- 但是C语言的所有编译器对进程函数的名字修饰并不涉及参数,只能通过函数名对不同的函数进行区分,故C语言不支持函数重载;
- 而C++的编译器对其函数进行的名字修饰会将参数类型包括,故C++支持函数重载。
【问题延伸】在实际中如果要实现一个静态库,但这个库可能被C语言工程和C++工程都调用,但编译器对两者的名字修饰规则并不相同,如何解决?
**extern “C”**解决
- 对于这种被C和C++都调用的库来说,都用C++实现库函数(因为C++兼容C语言)
- 在某个函数前加extern “C”,意思是告诉编译器,将该函数按照C语言规则来编译
5. 引用概念
是什么
- 引用不是新定义了一个变量,它的本质就是为已存在的变量起个别名,编译器不会为引用变量开辟新空间
- 它与它引用的变量共用同一块空间(空间一样因此值也一样)
- 格式
类型& 引用变量名 = 引用实体;
- & 出现在类型之后,为引用符
- 引用类型必须与其实体一致
例1:观察引用使用
#include<iostream>
#include<Windows.h>
using namespace std;
int main()
{
int a = 10;
int& ra = a;
cout <<"变量a的引用ra:"<< ra << endl;
system("pause");
return 0;
}
引用变量的特性
- 引用变量在定义时必须先初始化
- 一个变量可以有多个引用
- 引用一旦引用一个实体,再不能引用其他实体
- 引用变量与实体的生命周期:一般实体的生命周期较长
为什么存在引用?
- 引用本质是对一个变量的别名,基于这种特性引用可以实现函数传参时只有指针传址传参的特性;
- C语言中传址传参只能通过指针,但是操作指针有许多不便之处:指针需考虑合法性问题、指针可读性不高
- 【Tip】传值、传引用、传指针效率比较:传指针 ~~ 传引用 >> 传值(以值作为参数或者返回值类型时,会发生值临时拷贝,效率很低)
// 定义一个参数为引用类型的交换函数
void myChange(int& ra, int& rb)
{
// 引用变量的操作等价于原变量直接操作;
// 因为引用变量的地址与原变量相同;
int temp = ra;
ra = rb;
rb = temp;
}
int main()
{
int a = 10, b = 20;
myChange(a, b); //实参就是待交换的变量本身
cout << "a:" << a << " b:" << b << endl;
return 0;
}
引用和指针的区别
【联系】
- 在语法概念上,引用就是一个变量的别名,没有独立空间,和其引用实体共用同一块空间
- 但在底层实现上,引用和指针一样的!引用在底层是按照指针方式来实现的(从汇编代码可观测到,汇编代码与指针实现完全一样)
- 引用变量实际是有空间的,空间存放实体变量的地址(引用底层通过指针实现,该点仅作了解)
int&
引用 可以看作int* const
指针类型;
const int&
引用类型 可以看作const int* const
指针类型
【区别】(从概念、特性、应用来看)
- 引用在定义时必须初始化,指针没有要求;
- 引用在初始化时引用了一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体;
- 没有NULL引用,但是有NULL指针;
- 在sizeof中含义不同,引用结果为引用实体类型的大小,但指针始终是地址空间所占字节个数;
- 引用自增表示对引用的实体数值+1,指针自增表示指针向后偏移一个类型的大小;
- 有多级指针,但没有多级引用;
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理;
- 引用比指针使用起来相对较为安全 ;
常引用
常引用:即对常量进行引用。
常引用的使用方式:
- 对于一般常量
const int a = 10;
const int& ra = a;
(最常见的常引用) - 对于数字型常量:
const int& b = 10;
(传参常用) - 对于浮点型常量:(仅作知识点了解,实际不会这样使用)
double d = 12.34; const int& rd = d;
引用的使用场景
做参数
- 想要让形参修改实参,参数需要传指针,这时可以用引用代替;
- 若不想让实参修改,可以用常引用
- 形参就是实参的引用,操作形参就是操作实参
// 【注意】因为并没有二级引用的概念
// 所以参数需传一级指针时,参数引用的类型即为int ;
// 参数传二级指针时,参数引用的类型为int * 一级指针
// 定义一个参数为int*的引用类型的交换函数
void myChange(int*& rpa ,int*& rpb)
{
int* temp = rpa;
rpa = rpb;
rpb = temp;
}
int main()
{
int a = 1, b = 2;
int* pa = &a;
int* pb = &b;
// 调用该函数可实现两指针的内容交换
myChange(pa, pb);
return 0;
}
做返回值
引用类型当然可以作为返回值类型,但要特别注意这里有个小坑:
例1:一个经典的返回值为引用类型的函数调用
上述的例子看似正确,结果好像也没有问题,事实真是如此吗?我们再来看一个例子;
例2:揭露了例1是一个经典的引用错误使用案例
输出的结果让人很是奇怪,明明引用变量ret赋值了Add(1,2)的引用类型返回值,为什么仅加了一条语句Add(4, 5);
,结果就会变化?
例3:通过这个例3与例2应该意识到例1的”正确“结果只是一种机制巧合
这样的结果说明了输出错误的值具体是多少与增加的语句并无直接关系。那这三个例子的不同结果到底是为什么?引用类型返回值到底该如何使用?
我们从原理上来分析
学过操作系统内存原理我们知道,函数调用会在栈空间上开辟一个栈帧结构用来存放函数数据(包括main函数),当函数调用结束栈帧会自动释放,函数内的局部变量会随栈帧的消失而销毁。
具体解释详见下图:
【总结】
- 一般返回值类型不采用引用类型;
- 因为返回值为引用类型时,一定注意不能返回函数栈上的变量;
- 但可以改变调用函数中需传出变量的生命周期,比如加staic修饰改变量即可解决该问题。
6. 内联函数
内联函数是什么
以inline关键字修饰的函数叫做内联函数;编译时C++编译器会在调用内联函数的地方展开,不会进行函数调用,也没有函数压栈的开销,因此内联函数能提升程序的运行效率。
例1:普通函数调用情况(通过debug模式调试查看反汇编代码)
例2:inline修饰后函数调用情况(VS环境特性默认还是会调用,想让Inline起作用需修改设置,具体根据自己编译器配置,下面仅根据VS2013举例)
内联函数特性
- inline是以空间换时间的做法(和宏替换都有的缺陷),少了调用开销,但让程序规模变大;
- inline是个建议性关键字,向编译器建议不调用关联函数,直接就地展开(不同编译器自己按规则决定是否遵循建议);
- 内联函数具有文件作用域,必须在当前文件定义,不能跨文件分离使用(即内联函数没有入口地址);
- 可以将内联函数定义直接放在头文件当中,这样就可多函数文件调用这个内联函数。
为什么有内联函数
用来弥补C语言宏函数的缺陷:C语言的宏不涉及类型检测,仅在预处理阶段直接替换;
宏的优缺点
【优】
- 提高性能;
- 增强代码的复用性;
【缺】
- 不方便调试宏,因为预处理阶段进行了宏替换;
- 导致代码可读性差,可维护性差,容易误用;
- 没有类型安全的检查,安全性低;
- 会出现一些副作用,比如对宏变量使用是自增(减),就很可能出现功能性错误。
有一种特殊的宏就是宏函数,有简短语句组成
【优】
- 宏函数并不是真正意义上的函数,不会有函数调用,提高程序的运行效率;
- 提高代码的可读性;
【缺】
- 不会进行类型检测,代码安全性低;
- 在预处理阶段直接展开,不能调试;
- 每个使用部分都会展开,造成程序冗余;
c++有哪些技术可替换宏?
- 常量宏定义
(#define PI 3.14)
换用const修饰常量(const double PI = 3.14)
在c++中const修饰的内容已经是一个常量,而C语言中是一个不可被修改的量;
c++中,被const修饰的常量:具有宏替换特性(编译阶段替换,而不是在预处理阶段替换);
- 短小函数宏定义换用内联函数
在debug模式下不会展开,可以调试;
内联函数不会产生副作用;
7. 指针空值nullptr(C++11)
C语言的空指针
int* pr = NULL
(初始化指针时让其指向NULL值是一个好习惯,防止野指针);
但是,NULL本质是个宏定义,不同头文件NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量,会出现概念歧义;
因此C++11引入了关键字nullptr的概念:
C++的指针空值
- c++11中可用关键字nullptr用来表示空指针;
- 因为是关键字 ,使用nullptr不需要包含头文件;
- 在C++11中,sizeof(nullptr)与sizeof((void*)0)所占的字节数相同(都为4字节);
- 在C++使用关键字nullptr,而不使用NULL这个宏,可以提高代码的健壮性;
都看到这里了
彦祖点个赞再走吧~