1. 什么是C++?
1.1 初识C++
C语言是结构化和模块化的语言,适合处理较小规模的程序。对于复杂的问题,规模较大的程序,需要高度的抽象和建模时,C语言则不合适。为了解决软件危机,20世纪80年代,计算机界提出了OOP(object oriented programming:面向对象)思想,支持面向对象的程序设计语言应运而生。 1982年,Bjarne Stroustrup博士在C语言的基础上引入并扩充了面向对象的概念,发明了一 种新的程序语言。为了表达该语言与C语言的渊源关系,命名为C++。因此:C++是基于C语言而 产生的,它既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行面向对象的程序设计。
C++之父Bjarne Stroustrup
1.2 第一个C++程序
#include <iostream>
using namespace std;
int main()
{
cout << "hello world!" << endl;
return 0;
}
2. 命名空间——namespace
2.1 什么是命名空间?
在解答什么是命名空间之前,我们先从C语言引入这样一个问题:
#include <stdio.h>
int pow = 20;
int main()
{
printf("%d\n", pow);
return 0;
}
这样的一个C语言固然没什么问题,程序也能正常执行,可是当我们加入math.h这个头文件时就会发生这样的情况:
#include <stdio.h>
#include <math.h>
int pow = 20;
int main()
{
printf("%d\n", pow);
return 0;
}
发现程序报错了,pow重定义,也就是说我们的pow定义了两次,编译器分不清这里要打印的到底是函数pow还是变量pow了,见微知著:
在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存 在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化, 以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
2.2 命名空间的定义及使用
命名空间的定义就相当于一个建围墙的过程,将我们要用到的变量、函数、类型等定义到围墙之内。具体方法为:
namespace+命名空间名称+{...}
比如:
namespace xxk
{
int age = 22;
int height = 185;
}
那么命名空间定义完成后又要怎么使用呢?下面介绍几种使用方法:
方法1:加命名空间名称及作用域限定符(::):
#include <stdio.h>
namespace xxk
{
int age = 22;
int height = 185;
}
int main()
{ //printf("%d\n", age);//报错:age未定义
//printf("%d\n", height);//报错:height未定义
printf("%d\n", xxk::age);
printf("%d\n", xxk::height);
return 0;
}
方法2:使用using将命名空间中某个成员引入:
#include <stdio.h>
namespace xxk
{
int age = 22;
int height = 185;
}
using xxk::age;
using xxk::height;
int main()
{
printf("%d\n", age);
printf("%d\n", height);
return 0;
}
方法3:使用using namespace +命名空间名称引入:
#include <stdio.h>
namespace xxk
{
int age = 22;
int height = 185;
}
using namespace xxk;
int main()
{
printf("%d\n", age);
printf("%d\n", height);
return 0;
}
运行结果为:
需要注意的是:方法3虽然一劳永逸了,但是会出现:
有时就是需要使用到namespace之外的功能,可恰巧这个功能的名字与namespace之内的名字相同,如果稍不留意,或者多人协作,别人并不知晓,这必然会导致bug的产生。因此使用方法3的时候一定要注意这个问题。
3. C++的输入与输出
我们知道,C语言中最重要并且使用最多的功能无非就是输入(scanf)与输出(printf)函数了,那么C++的输入与输出又是怎么实现的呢?
其实在1.2中我们已经见到过了:
#include <iostream>
using namespace std;
int main()
{
cout << "hello world!" << endl;
return 0;
}
对第一个C++程序的解释:
1. 关于cout与cin的解释
(1)与printf与scanf相似, 在C++中使用cout与cin实现输出与输入,不过前两者与后两者的意义却大相径庭,万万不可与之做同等理解。
(2)使用cout与cin时,必须包含<iostream>头文件以及需要使用到名字为std的命名空间。
(可以理解为跟C语言一样,使用printf与scanf要包含头文件<stdio.h>,而C++中使用cout与cin要用到<iostream>头文件和std命名空间)(其实早期C++的头文件也以.h结尾,后为区分C语言与C++,规定C++的头文件不带.h)
(3)C语言输出和输入变量时需要指定数据类型,比如数值型%d,小数%f......,而C++在输出和输入时会自动判定数据类型,无需再手动输入。
2. 关于<<与endl的解释:
<<是流插入运算符,>>是流提取运算符。(后面会详细说明)
endl表示换行输出,与C语言地\n相似,endl也包含在头文件<iostream>中。
cout代码演示:
#include <iostream>
using namespace std;
int main()
{
int a = 22;
float w = 119.5;
cout << a << endl << w << endl;
return 0;
}
cin代码演示:
#include <iostream>
using namespace std;
int main()
{
int a = 0;
float w = 0;
cin >> a >> w;
cout << a << endl << w << endl;
return 0;
}
3. 注意:
需要注意的是,我们这里明显使用了2.2的方法3,直接using namespace std;虽然方便了,可是标准库里的所有东西都展现出来了。在2.2的末尾就提到过,所以在日常练习时,我们完全可以使用这种方法。可是在项目代码多,规模大的时候方法3显然就不合适了。
4. 函数重载
4.1 函数重载的概念
相同的,我们先从C语言中引入问题:我想写一个加法函数:
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}
int main()
{
printf("%d\n", add(2, 3));//结果为5
printf("%d\n", add(2.2, 3.3));//结果也为5
return 0;
}
观察如上代码,当我想得到小数加法时就必须再写一个小数加法的不同名函数才能得到正确结果,我们都是加法,凭什么你用了加法的名字,我就不能再用这个名字了呢?!
有没有方法解决这个问题呢?这时C++就为这个问题提供了解决方法:函数重载
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数或类型或类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。
4.2 函数重载的用法:
1. 参数类型不同:
#include <iostream>
using namespace std;
int add(int a, int b)
{
return a + b;
}
double add(double a, double b)
{
return a + b;
}
int main()
{
cout << add(2, 3) << endl;
cout << add(2.2, 3.3) << endl;
return 0;
}
2. 参数个数不同:
#include <iostream>
using namespace std;
int add(int a)
{
return a;
}
int add(int a, int b)
{
return a + b;
}
int add(int a, int b,int c)
{
return a + b + c;
}
int main()
{
cout << add(2) << endl;
cout << add(2, 3) << endl;
cout << add(2, 3, 4) << endl;
return 0;
}
3. 参数类型顺序不同
#include <iostream>
using namespace std;
int add(char a, int b)
{
cout << a << endl;
return b;
}
int add(int a, char b)
{
cout << b << endl;
return a;
}
int main()
{
cout << add(3, 'x') << endl;
cout << add('x', 3) << endl;
return 0;
}
5. 引用
5.1 引用的概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
简单而言,引用就是给别人取了一个绰号,一个人拥有多个名字。
引用的用法为:数据类型&引用变量名(对象名) = 引用实体;
#include <iostream>
using namespace std;
int main()
{
int a = 20;
int& b = a;
cout << a << endl << b << endl;
cout << &a << endl << &b << endl;
return 0;
}
5.2 引用常用的使用场景
1. 做参数
我们知道,在C语言的函数传参过程中,当实参传递给形参的时候,形参是实参的一份临时拷贝,所以对形参的修改不会对实参产生影响。故当时写交换函数时需要用到指针传参:
#include <stdio.h>
void swap(int* a, int* b)
{
int temp = *a;
*a = *b;
*b = temp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d%d", &a, &b);
printf("交换前:a=%d,b=%d\n", a, b);
swap(&a, &b);
printf("交换后:a=%d,b=%d\n", a, b);
return 0;
}
我们刚刚说过,引用就是给变量起了一个别名,不同的名字却是同一个人,不同的名字却拥有同样的地址,因此以引用做参数就可以避免在函数内重新定义变量的情况,故以引用做参数,形参是实参的引用,对形参的修改就可以做到真真切切地影响实参。
我们这时就可以这样写:
#include <iostream>
using namespace std;
void swap(int& a, int& b)
{
int temp = a;
a = b;
b = temp;
}
int main()
{
int a = 0;
int b = 0;
cin >> a >> b;
cout << "交换前a=" << a << "b=" << b << endl;
swap(a, b);
cout << "交换后a=" << a << "b=" << b << endl;
return 0;
}
也当然可以函数重载:
#include <iostream>
using namespace std;
void swap(int* a, int* b)
{
int temp = *a;
*a = *b;
*b = temp;
}
void swap(int& a, int& b)
{
int temp = a;
a = b;
b = temp;
}
int main()
{
int a = 0;
int b = 0;
cin >> a >> b;
cout << "交换前a=" << a << "b=" << b << endl;
swap(&a, &b);
swap(a, b);
cout << "交换后a=" << a << "b=" << b << endl;
return 0;
}
显然,在实际操作中, 后者地操作方法更方便,传值效率也更高。
2. 做返回值
思考如下代码的运行结果:
#include <iostream>
using namespace std;
int& add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& n = add(1, 2);
cout << "add(1, 2) = " << n << endl;
add(3, 4);
cout << "add(1, 2) = " << n << endl;
return 0;
}
运行结果:
这是为什么呢?按道理两次的运行结果都应该是3才对呀,为什么两次n的值不一样呢?
想要理解原因,那就需要深刻理解函数栈帧的知识了,稍后会有函数栈帧的详细讲解,还请大家多多关注后续文章。这里就先把原因道明:
1.函数运行时,系统需要给该函数开辟独立的栈空间,用来保存该函数的形参, 局部变量以及一些寄存器信息等;
2.函数运行结束后,该函数对应的栈空间就被系统回收了;
3.空间被回收指该块栈空间暂时不能使用,但是内存还在;比如:上课要申请教室,上完课之后教室归还给学校,但是教室本身还在,不能说归还了之后,教室就没有了。
因此:
如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。
6. 内联函数inline
在讲解内联函数之前,我们先回忆一下C语言中关于宏的知识。
6.1 写一个宏函数
#define add(x,y)((x)+(y))
#include <stdio.h>
int main()
{
int a = add(2, 5);
printf("%d\n", a);
return 0;
}
所以我们回忆起宏的优点:
1.增强代码的复用性;
2.不用建立栈帧,提高效率。
宏的缺点:
1.容易出错,语法细节多;
2.不能调试;
3.没有类型安全的检查。
所以,有没有一种方法既能结合宏的优点,又能规避宏的缺点呢?答案确实是有的——内联函数。
6.2 内联函数的概念
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。
比如:写一个普通函数:
#include <iostream>
using namespace std;
int add(int x, int y)
{
return x + y;
}
int main()
{
int a = add(2, 5);
cout << a << endl;
return 0;
}
这时再写一个内联函数:
#include <iostream>
using namespace std;
inline int add(int x, int y)
{
return x + y;
}
int main()
{
int a = add(2, 5);
cout << a << endl;
return 0;
}
6.3 内联函数的特性
1. inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。
2. inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。
3. inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。