C++入门
C++是在C的基础上容纳进去了类和对象的编程思想,并添加了许多有用的库,以及编程范式等。
一.C++关键字
C++ 有63个关键字,C有32个关键字。关键字的功能不在一一详讲,用的时候在进行细讲。
二.命名空间
C++/C 中,变量、函数和类的名大量存在,他们的名字会发生重复,为了能让重复的名同时存在创作出了命名空间。
举个会报错的例子:
#include<iostream>
#include<stdlib.h>
int malloc = 1;
int main()
{
printf("%d",malloc );
return 0;
}
//error C2365: “malloc”: 重定义;以前的定义是“函数”
1.命名空间的定义
用到 namespace 关键字,后面跟命名空间的名字,接着一个{}
namespace test
{
//命名空间内可以定义变量
int malloc = 1;
}
int main()
{
//使用命名空间test的变量
printf("%d",test:: malloc);
return 0;
}
namespace test
{
int malloc = 1;//命名空间内可以定义变量
//命名空间内可以定义结构体
struct test
{
int* arr;
int size;
};
//命名空间内可以定义函数,等等
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
}
2.命名空间的嵌套
命名空间中可以嵌套命名函数
namespace test_1
{
int i_1 = 1;
char arr_1[] = { 1,2,3 };
namespace test_2
{
int i_2 = 2;
char arr_2[] = { 4,5,6 };
}
}
3.命名空间的合并
在一个工程中,允许存在多个相同名称的命名空间,编译器编译时会把相同名称的命名空间合并成一个
如:前两个test命名空间会合并成一个(编译器合成)
4.命名空间的使用
有三种方式:
- 命名空间名称+作用域限定符
namespace test_3
{
int i_3 = 1;
}
int main()
{
//命名空间名称+成员
printf("%d ", test_3::i_3);
return 0;
}
- 使用 using 将某个命名空间的成员引入
namespace test_4
{
int arr[] = { 1,2,3,4,5 };
int i = 0;
}
//using+作用域限定符
using test_4::arr;
int main()
{
for (int i = 0; i < sizeof(arr) / sizeof(int); i++)
{
printf("%d ", arr[i]);
}
return 0;
}
- 使用 using 将某个命名空间的成员全部引入
//把std命名空间全部展开
using namespace std;
int main()
{
//cout和endl 是std命名空间的关键字
cout << "Hello World" << endl;
return 0;
}
三.C++输入&输出
- 1.使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时,必须包含< iostream >头文件以及按命名空间使用方法使用std。
- cout和cin是全局的流对象,end是特殊的C++符号,表示换行输出,他们都包含在包含头文件中。
- <<是流插入运算符, >>是流提取运算符。
- 使用C++输入输出更方便,不需要像printf/scanf输入输出时那样, 需要手动控制格式。C++的输入输出可以自动识别变量类型。
- 实际上cout和cin分别是ostream和istream类型的对象, >>和<<也涉及运算符重载等知识,这些知识我们我们后续才会学习,所以我们这里只是简单学习他们的使用。后面我们还有一个个章节更深入的学习流用法及原理。
注意:早期标准库将所有功能在全局域中实现,声明在h后缀的头文件中,使用时只需包含对应头文件即可,后来将其实现在std命名空间下,为了和C头文件区分,也为了正确使用命名空间,此推荐使用+std的方式。此推荐使用+std的方式。
#include<iostream>
//std 是C++标准库的命名空间,标准库的定义都在这个命名空间中
using namespace std;
int main()
{
int a ;
float b ;
char c;
cin >> a;
cin >> b >> c;
cout << "Hello World" << endl;
cout << "a:" << a << " b:" << b << " c:" << c << endl;
return 0;
}
四.缺省参数
1.概念
缺省参数是声明或定义函数时为函数的参数指定-个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。
2.全缺省参数
调用的函数不传一个参数
//打印出 10 20 30
void Function(int a = 10, int b = 20, int c = 30)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
int main()
{
Function();//不传参数
return 0;
}
3.半缺省参数
- 半缺省参数必须从右往左依次来给出,不能间隔着给
- 缺省参数不能在函数声明和定义中同时出现
- 缺省值必须是常量或全局变量
//打印出 1 2 30
void Function(int a = 10, int b = 20, int c = 30)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
int main()
{
Function(1,2);//不传参数
return 0;
}
五.函数重载
C语言中不允许重复命名函数,而C++在这里进行可改进
1.函数重载的概念
函数重载:是函数的- -种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数或类型或类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。
2.函数重载的用法
//函数名相同,形参类型不同
int Add(int x, int y)
{
return x + y;
}
float Add(float x, float y)
{
return x + y;
}
float Add(float x, int y)
{
return x + y;
}
int main()
{
int n_1 = 1;
int n_2 = 2;
float n_3 = 1.0;
float n_4 = 2.0;
Add(n_1, n_2);//int 和int 类型
Add(n_3, n_4);//float 和float类型
Add(n_3, n_1);//float 和 int 类型
return 0;
}
3.函数重载的原理
首先我们要了解一下一个程序运行起来要经历的四个阶段:
预处理 -> 编译 -> 汇编 -> 链接
C语言在汇编阶段中不会处理函数名,而C++在汇编阶段中会把函数名处理成其他形式
下面是在Linux环境下函数被处理的形式:
_Z + 名字符个数 + 函数名 + 类型名的首字母
//在C语言中:Add 在C++中是:_Z3Addii
int Add(int x, int y)
{
return x + y;
}
//在C语言中:Add 在C++中是:_Z3Addff
float Add(float x, float y)
{
return x + y;
}
//_Z3Addfi
float Add(float x, int y)
{
return x + y;
}
通过这样的处理C语言就无法支持函数重载,因为同名函数没有区分,编译器无法识别。而C++的同名函数进行了区分,编译器可以识别,可以支持函数重载。
六.引用
从语法语法角度来说 不开空间,实际上 开空间。
1.引用的概念
引用不是新创建一个变量,而是给已经存在的变量起一个别名
比如:周星驰 是 大名, 在电影中叫他 星星,粉丝喜欢叫他 星爷,无论叫哪一个,都指的是独一无二的他
2.引用类型的定义
用符号 & .
- 引用类型必须和引用实体是同类型
- 引用在定义时必须初始化
- 一个变量可以有多个引用
- 引用一旦引用一个实体,就不能在引用其他实体
int& i;//这个语句存在时编译会出错
int n = 0;
//引用 n,给n取一个别名 m 和 nm
int& m = n;
int& nm = n;
typedef struct Node
{
int a;
float b;
}Node;
Node test_1;
//引用 test_1,给test_1取一个别名test_2
Node& test_2 = test_1;
3 .引用的特殊使用场景
(1)做参数
//交换值不用再用指针,可以使用引用
int Swap(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 1;
int b = 2;
Swap(a, b);
return 0;
}
(2)做返回值
先给大家讲返回值的返回原理
给大家看一个错误代码:
int& Double_function(int n)
{
int Double = n * 2;
return Double;
}
int main()
{
int n = 2;
int ret = Double_function(n);
return 0;
}
错误的原因:
正确代码:
int& Double_Function(int n)
{
static int Double = n * 2;//Double储存在静态区
return Double;
}
int main()
{
int n = 2;
int ret = Double_Function(n);
return 0;
}
(3).引用和指针权限大小问题
权限只能缩小和保持,但权限不能放大
权限放大:
const int n = 0;
//权限放大
int& nn = n;
const int* p = NULL;
//权限放大
int* pp = p;
权限缩小和保持:
//权限缩小
int n = 0;
const int& nn = n;
const int* p = NULL;
const int* pp = p;
//权限保持
const int n = 0;
const int& nn = n;
const int* p = NULL;
const int* pp = p;
3.引用和指针的区别
在语法概念上,引用是一个别名,不占用空间,和引用实体共用一个空间。但是,引用在底层实现上是有空间的,因为引用是按指针的方式实现的
引用和指针的不同点:
- 引用概念上定义一个变量的别名,指针存储一个变量地址。
- 引用在定义时必须初始化,而指针没有要求。
- 引用在初始化时引用一个实体后不能再引用其他实体,而指针没要求
- 没有NULL引用,而有NULL指针
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全
七.内联函数
以 inline 修饰的函数叫做内联函数,编译时c++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率(类似c语言的定义宏define)
一般来说,内联机制用于优化规模较小,流程直接,频繁调用的函数,很多编译器都不支持内联函数递归。而且当一个内联函数的的函数太多时就不在实行内敛机制(在调用内联函数的地方展开)
内联函数不建议声明和定义分开,因为inline没有函数地址,会导致链接错误。直接在声明处定义函数(头文件处定义)
1.宏定义的缺点(define)
- 不方便调试宏。(因为预编译阶段进行了替换)
- 导致代码可读性差,可维护性差,容易误用。
- 没有类型安全的检查。
C++中可代替宏的技术:
- 常量定义用 const enum。
- 短小函数用内联函数定义。
#define Add(x,y) ((x)+(y))
int main()
{
int a = 1;
int b = 2;
int c = Add(a, b);
return 0;
}
2.内联函数的定义
//用inline修饰
inline int Add(int x, int y)
{
int c = x + y;
return 0;
}
int main()
{
int a = 1;
int b = 2;
int c = Add(a, b);
return 0;
}
八.auto关键字(C++11)
1.为什么有auto关键字
随着不断深入的学习,程序复杂度增加,类型名越来越长,体现在:
- 类型难于拼写
- 含义不不明确容易导致出错
auto关键字的优点:
- 增强代码的复用性
- 提高性能
举例:
int main()
{
std::map<std::string, std::string> m{{ "学号","123" }, {"姓名", "张三"}, { "性别", "男"}
};
//std::map<std::string, std::string> ::iterator 是迭代器类型
std::map<std::string, std::string> ::iterator it = m.begin();
//auto 自动识别类型
auto it = m.begin();
return 0;
}
2.回顾 typedef
C语言也有简化类型名的关键字 typedef,但是,typedef 也有缺点
给大家看个代码:
typedef int* ptr;
int main()
{
const ptr p1;//错误还是正确?
const ptr* p2;//错误还是正确?
return 0;
}
代码讲解
typedef int* ptr;
int main()
{
// 实际是 char* const ptr ,const 值必须初始化
const ptr p1;//错
// 实际是 const char** ptr ,是二级指针
const ptr* p2;//正确
return 0;
}
在编程时,常常需要把表达式的值赋值给变量,这就要求在声明变量的时候清楚地知道表达式的类型。然而有时候要做到这点并非那么容易,因此C++11给auto赋予了新的含义。
2.auto简介和使用
简介:
在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,但遗憾的是没有人去一直使用它
C++11中,标准委员会赋予了auto全新的含义即: auto不再是一一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
使用:
int ret()
{
return 9;
}
int main()
{
int n = 66;
auto n_1 = n;// int n_3 = 66;
auto n_2 = 'n';//char n_2 = "66";
auto n_3 = ret();// int n_3 = 9;
cout << typeid(n_1).name() << endl;
cout << typeid(n_2).name() << endl;
cout << typeid(n_3).name() << endl;
//auto i; 无法通过编译,定义 auto 变量必须初始化
return 0;
}
注意:
的实际类型。因此auto并非是-种"类型”的声明,而是一个类型声明时的”占位符",编译器在编的实际类型。因此auto并非是-种"类型”的声明,而是一个类型声明时的”占位符",编译器在编译期会将auto替换为变量实际的类型。
3.auto的规范使用
(1)auto和指针结合
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须 加&
int main()
{
int n = 1;
//auto 是int
auto& n_1 = n;
//auto 是int*
auto n_2 = &n;
//auto 是int*
auto* n_3 = &n;
cout << typeid(n_1).name() << endl;
cout << typeid(n_2).name() << endl;
cout << typeid(n_3).name() << endl;
return 0;
}
(2)在同一行内定义多个变量
int main()
{
int a = 1;
int b = 2;
char y = 'x';
//正确
auto a_1 = a, b_1 = b;
//错误
auto a_2 = a, y_2 = y;
//当在同一行声明多个变量时,这些变量必须是相同的类型,
//否则编译器将会报错,因为编译器实际只对第一个类型进行推导,
//然后用推导出来的类型定义其他变量。
return 0;
}
4.auto不能推导的场景
(1)auto不能作为函数的参数
错误代码:
void Function(auto n)
{
}
int main()
{
int n = 0;
Function(n);
return 0;
}
(2)不能直接用来声明数组
演示:
int main()
{
int arr_1[] = { 1,2,3,4 };
//错误代码
auto arr_2[] = { 1,2,3,4 };
return 0;
}
九.基于范围的for循环(C++11)
1.范围 for 的语法
在C++98中要遍历一个数组可以用以下方法:
int main()
{
int arr[] = { 1,2,3,4,5,6 };
//法一
for (int i = 0; i < sizeof(arr) / sizeof(int); i++)
{
cout << arr[i] << endl;
}
//法二
for (int* p = arr; p < arr + sizeof(arr) / sizeof(int); p++)
{
cout << *p << endl;
}
return 0;
}
C++11中引入了基于范围的for循环。for循环后的括号由冒号”:”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
代码演示:
int main()
{
int arr[] = { 1,2,3,4,5,6 };
//只是依次把arr的值赋值给i
//范围for常常和auto搭配使用
for (auto i : arr)
{
cout << i << endl;
}
return 0;
}
2.范围 for 使用条件
for循环迭代的范围必须是确定的
- 对于数组而言,就是数组中第一个元素和最后一个元素的范围;
- 对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
注意:以下代码就有问题,因为for的范围不确定
void Function(int* arr)
{
//这里的arr不能代表整个arr数组,
//数组传参传的只是数组首元素地址
for (auto i : arr)
{
cout << i << endl;
}
}
int main()
{
int arr[] = { 1,2,3,4,5,6 };
Function(arr);
return 0;
}
十.指针空值(C++11)
1.c++98中的空指针
在良好的C/C++编程环境中,声明一个变量时好给该变量一个合适的初始值,否则可能会出现不可预料的错误,比如未初始化的指针。如果一个指针没有合法的指向,我们基本都是按照如下方式对其进行初始化:
int main()
{
int* p1 = NULL;
int* p2 = 0;
return 0;
}
NULL实际是一个宏,在传统的头文件(stddef .h )可以看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL
#else
#define NULL ((void *)0)
可以看到,NULL可被定义为字面常量0,或被定义成无指针类型void*的常量
不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,
比如:
void Function(int)
{
cout << "Function(int)" << endl;
}
void Function(int*)
{
cout << "Function(int*)" << endl;
}
int main()
{
Function(0);
Function(NULL);
Function((int*)NULL);
return 0;
}
//打印结果
//Function(int)
//Function(int)
//Function(int*)
程序本意是想用NULL调用void Function(int*),但是NULL被定义为0,调用的是void Function(int)。
在C++98中,字面常量0既可以是一个整形数字,也可是无类型的指针void*…但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void*)0。
注意:
- 在使用nullptr表示指针空值时,不需要用头文件。因为nullptr是C++11作为关键之引入的。
- 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
- 为了提高代码的健壮性,在后续表示指针空值时建议 好使用nullptr。