目录
一、C++的输入和输出
现在尝试用C++写出第一个程序:
#include<iostream>
using namespace std;
int main()
{
cout << "hello world" << endl;
return 0;
}
说明:
1. 与C程序一样,首先我们需要包头文件:iostream --> 是输入输出流
2. <<
- 左移操作符
- 流插入 —— 自动识别类型(不需要手动控制格式)
3. cout
- c: console --> 控制台
- out:输出
- endl:endline --> '\n'
上面这个是输出操作,下面再来个输入:
#include<iostream>
using namespace std;
int main()
{
int i;
char ch;
cin >> i >> ch;
cout << d << endl;
return 0;
}
说明:
1. >>
- 右移操作符
- 流提取
2.cin
- c:console --> 控制台
- in: 输入
小扩展:
C++中 cout 如何控制小数精度
#include<iostream>
using namespace std;
int main()
{
double d = 1.11111;
cout.precision(2); // 圆括号里的数字即代表精确位数
cout << d << endl;
return 0;
}
这里暂时只做了解,因为涉及后面对象的知识。当然,C++兼容C,我们也可以用我们熟悉的printf函数来解决精度问题。
std 命名空间的使用惯例:std 是 C++ 标准库的命名空间,如何展开 std 使用更合理呢?1. 在日常练习中,建议直接 using namespace std 即可,这样就很方便。2. using namespace std 展开,标准库就全部暴露出来了,如果我们定义跟库重名的类型 /对 象/ 函数,就存在冲突问题。该问题在日常练习中很少出现,但是项目开发中代码较多、规模大,就很容易出现。所以建议在项目开发中使用,像std::cout 这样使用时指定命名空 +using std::cout展开常用的库对象 / 类型等方式。
二、缺省参数/默认参数
2.1 缺省参数的概念
缺省参数是 声明或定义函数时 为函数的 参数指定一个缺省值 。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。
缺省参数分为:全缺省 和 半缺省
注意:这里的 “半” 不是缺省一半的参数的意思,而是有多个参数缺省,但不是全部。
2.2 缺省参数的定义及应用
a.全缺省
#include<iostream>
using namespace std;
//全缺省
void Func(int a = 0)
{
cout << a << endl;
}
void Func2(int a = 10, int b = 20, int c = 30)
{
cout << "a = " << a << endl;
cout << "a = " << b << endl;
cout << "a = " << c << endl;
}
int main()
{
Func(1); //有参
Func(); //无参
Func2(1, 2, 3);
Func2(1, 2); // 不传全,默认从左向右依次覆盖
return 0;
}
b.半缺省
#include<iostream>
using namespace std;
// 半缺省 只能从右往左连续给
void Func2(int a, int b = 20, int c = 30)
{
cout << "a = " << a << endl;
cout << "a = " << b << endl;
cout << "a = " << c << endl;
}
int main()
{
Func2(1, 2, 3);
Func2(1, 2); // 不传全,默认从左向右依次覆盖
return 0;
}
注意:
1. 半缺省参数必须 从右往左依次 来给出,不能间隔着给2. 缺省参数不能在函数声明和定义中同时出现//a.hvoid Func ( int a = 10 );// a.cppvoid Func ( int a = 20 ){}// 注意:如果生命与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那个缺省值。3. 缺省值必须是常量或者全局变量4. C 语言不支持(编译器不支持)
三、函数重载
函数重载的概念
函数重载: 是函数的一种特殊情况, C++ 允许在 同一作用域中 声明几个功能类似 的同名函数 ,这些同名函数的形参列表 ( 参数个数 或 类型 或 类型顺序 ) 不同 ,常用来处理实现功能类似数据类型不同的问题。
/*
C语言不支持重载 链接时,直接用函数名去找地址,有同名函数,区分不开
Cpp如何支持的呢? 函数名修饰规则,函数名字中引入参数类型,各个编译器自己实现一套
*/
// 1、参数类型不同
int Add(int left, int right)
{
cout << "int Add(int left, int right)" << endl;
return left + right;
}
double Add(double left, double right)
{
cout << "double Add(double left, double right)" << endl;
return left + right;
}
// 2、参数个数不同
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a)" << endl;
}
// 3、参数类型顺序不同
void f(int a, char b)
{
cout << "f(int a,char b)" << endl;
}
void f(char b, int a)
{
cout << "f(char b, int a)" << endl;
}
int main()
{
Add(1, 2);
Add(1.1, 2.2);
f(1, 'a');
f('a', 1);
return 0;
}
四、引用
4.1 引用的概念
引用 不是新定义一个变量,而 是给已存在变量取了一个别名 ,编译器不会为引用变量开辟内存空间,它和它引用的变量 共用同一块内存空间。
4.2 引用类型的定义
类型 & 引用变量名 ( 对象名 ) = 引用实体;
void TestRef()
{
int a = 10;
int& ra = a;//<====定义引用类型
printf("%p\n", &a);
printf("%p\n", &ra);
}
注意: 引用类型 必须和引用 实体 是 同种类型 的
4.3 引用特性
1. 引用在 定义时必须初始化2. 一个变量可以有多个引用3. 引用一旦引用一个实体,再不能引用其他实体
void Test()
{
int a = 0;
//1.引用必须初始化
//int& b; 这样写会报错,错误语法
//2.引用定义后,不能改变指向
int& b = a;
int c = 2;
//b = c; 这样写就变成了把c的值赋给b(a)了
//3.一个变量可以有多个引用(多个别名)
int& d = a;
return 0;
}
4.4 引用和指针的关系
- 指针和引用的功能是类似的、重叠的。
- C++的引用,是对指针使用比较复杂的场景进行一些替换,让代码更简单移动,但是不能完全替代指针。
- 引用不能完全替代指针的原因:引用定义后,不能改变指向。
- 比如在做数据结构,链表的操作等,引用就不适用了,因为需要用指针改变指向。
4.5 引用的使用场景
4.5.1 做参数
//1.做参数(a、输出型参数, b、对象较大时,减少拷贝,提高效率)
// 这些效果,指针也可以,但是引用更方便
void Swap(int& a, int& b)
{
int temp = a;
a = b;
b = temp;
}
int mian()
{
int x = 0, y = 1;
Swap(x, y);
return 0;
}
4.5.2 做返回值
int& Count()
{
static int n = 0;
n++;
// ...
return n;
}
注意:
如果函数返回时,出了函数作用域,如果返回对象还在 ( 还没还给系统 ) ,则可以使用 引用返回,如果已经还给系统了,则必须使用传值返回。
4.6 引用和指针的区别
语法上:
- 引用是别名,不开空间;指针是地址,需要开空间存地址
- 引用必须初始化,指针可以初始化也可以不初始化
- 引用不能改变指向
- 引用相对更安全,没有空引用,但是有空指针,容易出现野指针,但是不容易出现野引用
- sizeof、++、解引用访问登方面的区别
用法和底层:
- 汇编层面上,没有引用,斗是指针,引用编译后也转换成指针了
五、内联函数
如需频繁调用一个函数,则需要频繁建立栈帧,在C语言中,可以用宏函数的方式来解决这个问题。
首先来复习一下宏函数:
// 容易犯错注意点:
// 1. 宏函数不是函数
// 2. 宏定义不要带 ';'
// 3. 括号控制优先级
// 核心店:宏是预处理阶段进行替换
#define ADD(a, b) ((a)+(b))
// 为什么要给 a b 分别加括号?
// 因为传过来的 a b 可能是一个表达式
宏的缺点:
- 语法复杂,坑很多,不容易控制
- 不能调试
- 没有类型安全的检查
于是C++就对这个问题进行了优化,我们看看C++如何解决的。
5.1 概念
以
inline
修饰
的函数叫做内联函数,
编译时
C++
编译器会在
调用内联函数的地方展开
,没有函数调
用建立栈帧的开销,内联函数提升程序运行的效率。
如果在上述函数前增加
inline
关键字将其改成内联函数,在编译期间编译器会用函数体替换函数的
调用。
查看方式:
1.
在
release
模式下,查看编译器生成的汇编代码中是否存在
call Add
2.
在
debug
模式下,需要对编译器进行设置,否则不会展开
(
因为
debug
模式下,编译器默认不
会对代码进行优化,以下给出
vs2013
的设置方式
)
5.2 特性
1. inline 是一种 以空间换时间 的做法,如果编译器将函数当成内联函数处理,在 编译阶段,会用函数体替换函数调用 ,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。2. inline 对于编译器而言只是一个建议,不同编译器关于 inline 实现机制可能不同 ,一般建议:将 函数规模较小 ( 即函数不是很长,具体没有准确的说法,取决于编译器内部实现 ) 、 不是递归、且频繁调用 的函数采用 inline 修饰,否则编译器会忽略 inline 特性。下图为《 C++prime 》第五版关于 inline 的建议:3. inline 不建议声明和定义分离,分离会导致链接错误。因为 inline 被展开,就没有函数地址了,链接就会找不到。
// F.h
#include <iostream>
using namespace std;
inline void f(int i);
// F.cpp
#include "F.h"
void f(int i)
{
cout << i << endl;
}
// main.cpp
#include "F.h"
int main()
{
f(10);
return 0;
}
// 链接错误:main.obj : error LNK2019: 无法解析的外部符号 "void __cdecl
// f(int)" (?f@@YAXH@Z),该符号在函数 _main 中被引用
六、关键字 auto
6.1 auto简介
在早期 C/C++ 中 auto 的含义是:使用 auto 修饰的变量,是具有自动存储器的局部变量 ,但遗憾的是一直没有人去使用它,大家可思考下为什么?C++11 中,标准委员会赋予了 auto 全新的含义即: auto 不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器, auto 声明的变量必须由编译器在编译时期推导而得 。
【注意】
使用 auto 定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导 auto的实际类型 。因此 auto 并非是一种 “ 类型 ” 的声明,而是一个类型声明时的 “ 占位符 ” ,编译器在编译期会将 auto 替换为变量实际的类型 。
6.2 auto的使用细则
6.2.1 auto与指针和引用结合起来使用
用 auto 声明指针类型时,用 auto 和 auto* 没有任何区别,但用 auto 声明引用类型时则必须加 &
int main()
{
int x = 10;
auto a = &x;
auto* b = &x;
auto& c = x;
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
*a = 20;
*b = 30;
c = 40;
return 0;
}
6.2.2 在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量 。
void TestAuto()
{
auto a = 1, b = 2;
auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}
6.3 auto不能推导的场景
6.3.1 auto不能作为函数的参数
// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{}
6.3.2 auto不能直接用来声明数组
void TestAuto()
{
int a[] = {1,2,3};
auto b[] = {4,5,6};
}
6.3.3
为了避免与 C++98 中的 auto 发生混淆, C++11 只保留了 auto 作为类型指示符的用法
6.4.4
auto 在实际中最常见的优势用法就是跟以后会讲到的 C++11 提供的新式 for 循环,还有lambda表达式等进行配合使用。
七、基于范围的for循环(C++11)
7.1 范围for的语法
对于一个 有范围的集合 而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此 C++11 中引入了基于范围的 for 循环。 for 循环后的括号由冒号 “ : ” 分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围 。
void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
for(auto& e : array)
e *= 2;
for(auto e : array)
cout << e << " ";
return 0;
}
注意:与普通循环类似,可以用 continue 来结束本次循环,也可以用 break 来跳出整个循环 。
7.2 范围for的使用条件
7.2.1 for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围 ;对于类而言,应该提供begin和 end 的方法, begin 和 end 就是 for 循环迭代的范围。
注意:以下代码就有问题,因为
for
的范围不确定
void TestFor(int array[])
{
for(auto& e : array)
cout<< e <<endl;
}
7.2.2 迭代的对象要实现++和==的操作
迭代器的问题,会在以后得章节进行详细讲解。
八、指针空值nullptr(C++11)
8.1 C++98中的指针空值
在良好的 C/C++ 编程习惯中,声明一个变量时最好给该变量一个合适的初始值,否则可能会出现不可预料的错误,比如未初始化的指针。如果一个指针没有合法的指向,我们基本都是按照如下方式对其进行初始化:
void TestPtr()
{
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*) 的常量 。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如:
void f(int)
{
cout<<"f(int)"<<endl;
}
void f(int*)
{
cout<<"f(int*)"<<endl;
}
int main()
{
f(0);
f(NULL);
f((int*)NULL);
return 0;
}
程序本意是想通过 f(NULL) 调用指针版本的 f(int*) 函数,但是由于 NULL 被定义成 0 ,因此与程序的初衷相悖。在 C++98 中,字面常量 0 既可以是一个整形数字,也可以是无类型的指针 (void*) 常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void*)0。
注意:1. 在使用 nullptr 表示指针空值时,不需要包含头文件,因为 nullptr 是 C++11 作为新关键字引入 的 。2. 在 C++11 中, sizeof(nullptr) 与 sizeof((void*)0) 所占的字节数相同。3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用 nullptr。