C/C++学习笔记(2020.11---2021.5)
标签: C/C++学习笔记(2020.11---2021.5) JavaScript博客 51CTO博客
2023-04-05 18:23:48 150浏览
CPP旅途
- C/C++语言的诞生
- C
- CPlusPlus
- 标准库STL版本
- HP(Hewlett-Packard) STL
- SGI (Silicon Graphics Computer System, Inc)STL
- STLport
- RW(Rouge Wave)实现版本
- P.J.Plauger实现版本
- 非标准库
- 程序运行过程
- 预处理
- 生成汇编指令
- 生成二进制文件
- 生成linking文件
- 静态链接库和动态链接库
- 动态链接库在Linux下的使用
- GNU编译器集合和LLVM项目
- libc、glibc、libstdc++、libc++
- gcc和g++标准库在计算机位置
- Linux/UNIX
- Windows
- 命名空间
- 定义命名空间
- using指令
- using关键字的其他作用
- 不连续的命名空间
- 嵌套的命名空间
- C预处理器
- define
- 在define中使用参数
- ##运算符
- 变参宏
- #undef指令
- 条件编译
- #ifdef、#else、#endif指令
- #ifndef指令
- #if和#elif指令
- #error
- #line
- #pragma
- 基本数据类型和表达式
- 基本数据类型
- 输出数据大小
- 整形
- 无符号和有符号
- 进制转换
- 实型
- 讲解浮点数的构造
- 字符型
- wchar_t宽字符
- 转义字符
- 布尔型
- 其他简单数据类型
- 可移植类型:stdint.h和inttypes.h
- 运算符与表达式
- 算数运算符
- 前置递增和后置递增
- 赋值运算符
- 比较运算符
- 逻辑运算符
- 位运算
- 按位取反
- 按位与
- 按位或
- 移位
- 异或运算
- 类型转换
- C的强制类型转换
- CPP的强制类型转换
- static_cast 静态转换
- dynamic_cast 动态转换
- const_cast 常量转换
- reinterpret_cast 重新解释转换
- 字符串和格式化输入/输出
- 字符串
- 字符串和字符的区别
- string.h头文件
- strlen()函数
- strcmp()函数
- strcat()函数
- 常量和C预处理
- define
- const
- 明示常量:limits.h和float.h
- printf()和scanf()
- printf()
- printf()有返回值
- sprintf()
- scanf()
- scanf()的返回值
- 字符输入/输出和输入验证
- getchar()和putchar()
- 缓冲区
- 缓冲区分类
- gets()和puts()
- cin 和 cout
- 使用cout进行输出
- 重载的<<运算符
- put()和write()方法
- 刷新缓冲区
- 用cout进行格式化
- 修改计数系统
- 调整字符宽度
- 填充字符
- 设置浮点数的显示精度
- 打印末尾的0和小数点
- setf()
- 使用cin进行输入
- cin如何检查输入
- 流状态
- 其他istream类方法
- get(char&)和get(void)
- getline()、get()
- 程序流程控制
- 选择结构
- 循环结构
- 跳转语句
- 数组、函数和结构体
- 数组
- 一维数组
- 二维数组
- 二维数组的其他表示方法
- 字符数组
- 函数
- 函数的定义
- 函数的调用
- 函数的分类
- 函数作为参数传参
- 函数重载
- 内联函数
- lambda匿名函数
- 捕获列表 []
- 形参列表 ()
- 返回类型 ->
- 函数体 {}
- 说明符
- 结构体
- 结构体的定义和使用
- typedef重命名
- 结构体的分类
- 结构体数组:静态创建
- 结构体指针:动态创建
- 结构体的妙用
- 结构体嵌套结构体
- 结构体做函数参数
- 结构体中const的使用场景
- 结构体数据顺序对内存影响
- enum枚举类型
- union共用体
- 结构体、枚举类型、共用体占用内存
- 指针、引用和内存分区模型
- 指针
- 指针变量的定义和使用
- 指针的关系运算
- 指针所占内存空间
- 空指针和野指针
- 空指针
- 野指针
- 指针常量和常量指针
- 指针和数组
- 数组指针
- 指针和函数
- 函数参数
- 函数返回值
- 函数自身作为参数
- 智能指针
- auto_ptr
- unique_ptr
- shared_ptr
- weak_ptr
- 引用
- 引用的基本使用:
- 引用的注意事项
- 引用做函数参数
- 引用函数做返回值
- 常量引用
- 指针和引用的区别
- 内存分区模型
- 内存分区
- 代码区
- 全局区
- 栈区
- 堆区
- malloc、calloc和free
- malloc和calloc区别
- new和delete
- new operator和operator new
- 剖析delete
- 文件输入、输出和异常
- C
- 理解I/O和内核的关系
- 与文件进行通信
- 文件分类
- I/O的级别
- 标准文件
- 标准I/O
- fopen()函数
- getc()和putc()函数
- fclose()函数
- 文件I/O
- fprintf()和fscanf()函数
- fgets()和fputs()函数
- 其他标准I/O函数
- ungetc(int c,FILE* fp)函数
- int fflush()函数
- fwrite()和fread()函数
- perror()
- CPP
- 写文件
- 读文件
- 对二进制文件进行写入
- 对二进制文件进行读取
- 异常
- exit()和abort()
- 异常机制
- 抛出异常:throw
- 捕获异常
- 栈解退
- exception类
- stdexcept异常类
- bad_alloc和new
- 空指针和new
- 异常的接口声明
- 类和对象
- 封装
- 访问权限
- 构造函数和析构函数
- 构造函数的分类及调用
- 构造函数分类
- 无参构造
- 有参构造
- 单参数构造:explicit关键字
- 拷贝构造
- 移动构造
- 三种调用方式
- 拷贝构造函数调用时机
- 构造函数调用规则
- 类成员作为类对象
- 静态成员static
- static使用要求
- this指针概念
- const修饰成员函数
- 友元
- 运算符重载
- 一元运算符重载
- 二元运算符重载
- 关系运算符重载
- 赋值运算符重载
- ++ 和 -- 运算符重载
- 输入、输出运算符重载
- 函数调用()运算符重载
- 不要重载&&、||和,操作符
- 继承
- 继承方式
- 继承中的构造和析构顺序
- 多态
- 多态分类
- 静态多态和动态多态区别
- 多态的原理剖析
- 虚函数
- 虚函数表
- 虚函数指针
- 多重继承之虚基类
- 纯虚函数和抽象类
- 虚析构和纯虚析构
时间,每天得到的都是24小时,可是一天的时间给勤勉的人带来智慧与力量,给懒散的人只能留下一片悔恨。 --鲁 迅
参考书籍:
- 《C++PrimerPlus》
- 《STL源码剖析》
- 《深度探索C++对象模型》
- 《MoreEffectiveC++》
- 《数据结构(C++语言版)》
- 《C++语言程序设计》
- …
学习C++永无止境,往前冲吧少年!
TIP:本博客主要以《C++语言程序设计》知识点内容为主,以在Linux/UNIX环境下进行编程,以及在此基础上会补充一些相关知识。
C/C++语言的诞生
TIP:UNIX比C先诞生
1971年,汤普逊(Ken Thompson)和里奇(Dennis Ritchie)共同发明了C语言,因为其高移植性,迅速风靡起来,1973年,UNIX正式由C改写。
C
C语言版本:
- 1989 年,C 语言被米国国家标准协会(American National Standards Institute, ANSI)标准化,称为 ANSI C 或 C89。
- 1990 年,国际标准化组织(International Organigation for Standardization, ISO)在 ANSI C 的标准上对 C 语言做了进一步修改,称为 ISO C 或 C90。
- 以此类推,之后又有 C99 (1999年)和 C11(2011年)标准诞生。
C语言有很多优点,也有很多不足。C++诞生了!
CPlusPlus
C++语言版本:
- 1980 年,贝尔实验室的 Bjarne Stroustmp 开始对C语言进行改进,为其加入面向对象的特性。最初,这种新语言被称为“带类的C(C with Classes)”。
- 1998 年,ANSI(美国国家标准协会)和 ISO(国际标准化组织)联合发布了至今使用最为广泛的 C++ 标准,称为 C++ 98。C++ 98 最为重大的改进就是加入了 “标准模板库”(Standard Template Library, STL),使得“泛型程序设计”成为 C++ 除“面向对象”外的另一主要特点。
- 2003 年,ISO 的 C++ 标准委员会又对 C++ 略做了一些修订,发布了 C++ 03 标准。C++ 03 和 C++ 98 的区别对大多数程序员来说可以不必关心。(小幅修改)
- 2011 年 9 月,ISO 通过了新的 C+ + 标准,这就是 C++11。(大改动)
- C++14、C++17、C++20
c++98: #define __cplusplus 199711L
c++03: #define __cplusplus 199711L
c++11: #define __cplusplus 201103L
c++14: #define __cplusplus 201402L
c++17: #define __cplusplus 201500L
标准库STL版本
HP(Hewlett-Packard) STL
HP STL是Alexandar Stepanov在惠普Palo Alto实验室工作时,与Meng Lee合作完成的。HP STL是C++ STL的第一个实现版本,而且是开放源码。其它版本的C++ STL一般是以HP STL为蓝本实现出来的。
SGI (Silicon Graphics Computer System, Inc)STL
SGI版本由Silicon Graphics Computer System, Inc公司发展,继承HP版本。所以它的每一个头文件也都有HP的版本声明。此外还加上SGI的公司版权声明。主要设计者仍然是STL之父Alexandar Stepanov,被Linux的C++编译器GCC所采用。SGI STL是开源软件,源码可读性非常高
STLport
为了使SGI STL的基本代码都适用于VC++和C++ Builder等多种编译器,俄国人Boris Fomitchev建立了一个free项目来开发STLport,此版本STL是开放源码的。(似乎是不再更新,并不支持C++11)
RW(Rouge Wave)实现版本
Rouge Wave版本是由Rouge Wave公司开发,它继承HP版本,并不属于open source范畴。Rouge Wave版本被C++Builder采用。(C++Builder是一款图形化集成开发环境)
P.J.Plauger实现版本
P.J.Plauger版本是由P.J.Plauger开发,它继承HP版本,并不属于open source范畴。此版本被用于Microsoft的Visual C++中,可读性简直是一坨屎。(哈哈哈哈深受Microsoft visual C++)
非标准库
TR1
TR1:C++ Technical Report 1 (TR1)是ISO/IEC TR 19768, C++ Library Extensions(函式库扩充)的一般名称。TR1是一份文件,内容提出了对C++标准函式库的追加项目。这些追加项目包括了正则表达式、智能指针、哈希表、随机数生成器等。TR1自己并非标准,他是一份草稿文件。然而他所提出的项目很有可能成为下次的官方标准。这份文件的目标在于「为扩充的C++标准函式库建立更为广泛的现成实作品」。
TR2
TR2:TR1的进阶版
BOOST
Boost是为C++语言标准库提供扩展的一些C++程序库的总称。Boost库是一个可移植、提供源代码的C++库,作为标准库的后备,是C++标准化进程的开发引擎之一,是为C++语言标准库提供扩展的一些C++程序库的总称。
Boost库由C++标准委员会库工作组成员发起,其中有些内容有望成为下一代C++标准库内容。在C++社区中影响甚大,是不折不扣的“准”标准库。
程序运行过程
C编程的基本策略是,用程序把源代码文件转换成可执行文件
预处理
预处理命令主要有三个部分:条件编译,文件引入,宏展开
main.c -> gcc -E -> main.i
生成汇编指令
编译指的是把源代码翻译成汇编指令的过程
main.i -> gcc -S -> main.s
生成二进制文件
把汇编指令翻译成机器语言,即二进制
Windows下是.obj,Linux/UNIX下是.o
main.s -> gcc -c -> main.o
生成linking文件
把二进制文件转换成可链接文件,此过程中会把静态链接库加入其中
Windows下是.exe,Linux/UNIX下是.out,运行文件的时候会把动态链接库相连
./a.out
静态链接库和动态链接库
- 静态链接库后缀一般为.lib(Windows)或.a(Linux)
制作静态链接库
// 此处编写规范lib+
ar rcs libcalc.a 原始二进制文件
引用静态链接库
gcc main.c -o main -L include/ -lcalc
- 动态链接库后缀一般为.dll(Windows)或.so(Linux)
制作动态链接库
g++ -fPIC -c libadd.cpp -I head/ // 生成.o
g++ -shared -o libadd.so libadd.o // 生成.so
引用动态链接库
// 编写规范lib+
gcc main.c -o main -L include/ -lcalc
静态链接库和动态链接库的区别:
- 由于静态库是在编译期间直接将代码合到可执行程序中,而动态库是在执行期时调用DLL中的函数体,所以执行速度比动态库要快一点;
- 静态库链接生成的可执行文件体积较大,且包含相同的公共代码,造成内存浪费;
- 使用动态链接库的应用程序不是自完备的,它依赖的DLL模块也要存在,如果使用载入时动态链接,程序启动时发现DLL不存在,系统将终止程序并给出错误信息。而使用运行时动态链接,系统不会终止,但由于DLL中的导出函数不可用,程序会加载失败;
- DLL文件与EXE文件独立,只要输出接口不变(即名称、参数、返回值类型和调用约定不变),更换DLL文件不会对EXE文件造成任何影响,因而极大地提高了可维护性和可扩展性,适用于大规模的软件开发,使开发过程独立、耦合度小,便于不同开发者和开发组织之间进行开发和测试。
动态链接库在Linux下的使用
但是一般使用的时候都还是找不到动态库的位置!!!
在此先介绍一个命令echo:输出字符串或变量
// 输出字符串
➜ C echo Hello World
Hello World
// 输出变量
➜ C echo $HOME
/home/liuhao
查看一个程序链接库位置的命令:ldd
➜ C ldd test
linux-vdso.so.1 (0x00007fff19dfe000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f0d142a2000) // C语言链接库
/lib64/ld-linux-x86-64.so.2 (0x00007f0d144a3000) // 链接器
其实系统是有自动寻找链接库的位置的,一般是在
// 第一个位置,这是./usr/lib的软链接
cd ./lib
// 第二个位置
cd ./usr/lib
所以给这个动态链接库生成一个软链接(相当于一个快捷方式)
// 放置的都是绝对路径
sudo ln -s ~/code/C/lib/libc.so /lib/lib.so
设置环境变量
// LD_LIBRARY_PATH查看变量
echo $LD_LIBRARY_PATH
// 给动态链接库设置库环境变量
export LD_LIBRARY_PATH=/home/code/C/lib:$LD_LIBRARY_PATH
但是这种方法是一次性的,关机就要重新设置了,不推荐
第三种方法
打开/etc/ld.so.conf
➜ /etc sudo vim ld.so.conf
include /etc/ld.so.conf.d/*.conf
我们只需要给他加上绝对路径即可
/home/liuhao/code/C/lib
生效
ldconfig -v
GNU编译器集合和LLVM项目
GNU项目起始于1987年,是一个开发大量免费UNIX软件的集合:GNU‘s Not UNIX,也被称为GCC,他的C编译器紧跟C标准的改动,用gcc命令便可调用GCC C编译器,许多使用cc作为gcc别名。
LLVM项目成为cc的另一个替代品,他的Clang编译器处理C代码,可以通过调用clang调用
gcc和clang命令都可以根据不同的版本选择运行时选项来调用不同C标准
gcc -std=c99 inform.c
gcc -std=c1x inform.c
gcc -std=c11 inform.c
libc、glibc、libstdc++、libc++
libc是Linux下原来的标准C库,后来逐渐被glibc取代,也就是传说中的GNU C Library,在此之前除了有libc,还有klibc,uclibc。现在只要知道用的最多的是glibc就行了,主流的一些linux操作系统如 Debian, Ubuntu,Redhat等用的都是glibc。
C++代码,还有两个库也要非常重视了,libc++/libstdc++,这两个库有关系吗?有。两个都是C++标准库。
libc++是针对clang编译器特别重写的C++标准库,那libstdc++自然就是gcc的事儿了
那g++是做什么的? 慢慢说来,不要以为gcc只能编译C代码,g++只能编译c++代码。 后缀为.c的,gcc把它当作是C程序,而g++当作是c++程序;后缀为.cpp的,两者都会认为是c++程序,注意,虽然c++是c的超集,但是两者对语法的要求是有区别的。在编译阶段,g++会调用gcc,对于c++代码,两者是等价的,但是因为gcc命令不能自动和C++程序使用的库联接,需要这样,gcc -lstdc++, 所以如果你的Makefile文件并没有手动加上libstdc++库,一般就会提示错误,要求你安装g++编译器了
// 使用cpp标准库
gcc demo.cpp -libstdc++
gcc和g++标准库在计算机位置
Linux/UNIX
usr:UNIX Software Resource
- c: usr/include/
- cpp:usr/include/c++/9/bits
C++17:
gcc7完全支持,gcc6和gcc5部分支持,gcc6支持度当然比gcc5高,gcc4及以下版本不支持。
C++14:
gcc5就可以完全支持,gcc4部分支持,gcc3及以下版本不支持。
C++11:
gcc4.8.1及以上可以完全支持。gcc4.3部分支持,gcc4.3以下版本不支持。
高版本的gcc向下兼容,支持低版本的C++标准。
Windows
VS2019\Visual Studio IDE\VC\Tools\MSVC\14.28.29910\include
VC全名是Microsoft Visual C++是微软出的一个集成的c,c++开发环境,比较经典版本是97年出的 Microsoft Visual C++ 6.0,不过目前最好还是用VS2008以上的版本中的VC了。
VS全名是Microsoft Visual Studio目前已经出到2017了,是很大的一个开发环境,包含很多高级语言的开发环境,VC、VB等,VC只是VS其中的一个开发环境。
vc版本与vs版本对应关系如下所示:
Visual Studio 6 : vc6
Visual Studio 2003 : vc7
Visual Studio 2005 : vc8
Visual Studio 2008 : vc9
Visual Studio 2010 : vc10
Visual Studio 2012 : vc11
Visual Studio 2013 : vc12
Visual Studio 2015 : vc14
Visual Studio 2017 : vc15
C++17:
vs2017基本支持,vs2015部分支持。
C++14:
vs2017就可以完全支持,vs2015基本支持,vs2013部分支持。
C++11:
vs2015及以上可以完全支持。vs2013基本支持,vs2012部分支持,vs2010及以下版本不支持。
命名空间
命名空间可作为附加信息来区分不同库中相同名称的函数、类、变量等。使用了命名空间即定义了上下文。本质上,命名空间就是定义了一个范围。
定义命名空间
命名空间的定义使用关键字 namespace,后跟命名空间的名称,如下所示:
namespace namespace_name {
// 代码声明
}
为了调用带有命名空间的函数或变量,需要在前面加上命名空间的名称,如下所示:
name::code; // code 可以是变量或函数
#include <iostream>
using namespace std;
// 第一个命名空间
namespace first_space{
void func(){
cout << "Inside first_space" << endl;
}
}
// 第二个命名空间
namespace second_space{
void func(){
cout << "Inside second_space" << endl;
}
}
int main ()
{
// 调用第一个命名空间中的函数
first_space::func();
// 调用第二个命名空间中的函数
second_space::func();
return 0;
}
using指令
您可以使用 using namespace 指令,这样在使用命名空间时就可以不用在前面加上命名空间的名称。这个指令会告诉编译器,后续的代码将使用指定的命名空间中的名称。
#include <iostream>
using namespace std;
// 第一个命名空间
namespace first_space{
void func(){
cout << "Inside first_space" << endl;
}
}
// 第二个命名空间
namespace second_space{
void func(){
cout << "Inside second_space" << endl;
}
}
using namespace first_space;
int main ()
{
// 调用第一个命名空间中的函数
func();
return 0;
}
using关键字的其他作用
给变量起别名,相当于typedef
// 给变量起别名,相当于typedef
using buyuan = void*;
引用基类成员
class A {
A();
~A();
};
class B: public A {
using A:A;
};
当然,using引用基类不止这么简单,基类中的size()是public,派生类继承的方式是private,所以size()也是private,想让他变成public,直接用using用法
class A {
public:
void size();
};
class B: private A {
using A:size;
};
不连续的命名空间
命名空间可以定义在几个不同的部分中,因此命名空间是由几个单独定义的部分组成的。一个命名空间的各个组成部分可以分散在多个文件中。
所以,如果命名空间中的某个组成部分需要请求定义在另一个文件中的名称,则仍然需要声明该名称。下面的命名空间定义可以是定义一个新的命名空间,也可以是为已有的命名空间增加新的元素:
namespace namespace_name {
// 代码声明
}
嵌套的命名空间
命名空间可以嵌套,您可以在一个命名空间中定义另一个命名空间,如下所示:
namespace namespace_name1 {
// 代码声明
namespace namespace_name2 {
// 代码声明
}
}
C预处理器
define
定义宏,来在预处理阶段的时候用替换体替换宏
#define NAME 1
在define中使用参数
#define A(X, Y) ((X) < (Y))?X:Y
##运算符
#define NAME(N) x ## n
NAME(1) = 14; // x1 = 14
NAME(2) = 13; // x2 = 13
变参宏
此处只介绍va_list变参
void va_start(va_list ap, last_arg);
type va_arg(va_list ap, type)
void va_end(va_list ap)
代码示例
#include <stdarg.h>
#include <stdio.h>
int mul(int, ...);
int main()
{
printf("15 * 12 = %d\n", mul(2, 15, 12) );
return 0;
}
int mul(int num_args, ...)
{
int val = 1;
va_list ap; // 定义一个指针
int i;
va_start(ap, num_args); // 让指针绑定变参
for(i = 0; i < num_args; i++)
{
val *= va_arg(ap, int); // 获取参数并运算
}
va_end(ap); // 解除参数绑定
return val;
}
其他可见stdarg.h头文件
#undef指令
#define A 10 // 定义宏A
#undef A // 取消宏A的定义
条件编译
#ifdef、#else、#endif指令
#ifdef A
#include "B.h" // 如果已经定义了宏A,执行
#else
#include "A.h" // // 如果没有定义宏A,执行
#endif
#ifndef指令
与ifdef类似,通常用语防止多次包含一个文件
#ifndef __STDIO_H__
#define __STDIO_H__
// do something
#endif
#if和#elif指令
#if A == 1
// do something
#elif A == 2
// ...
#else
// ...
#endif
#error
让预处理器发出一条错误消息
#if __STDC_VERSION__ != 201112L
#error NOT C11
#line
重置__LINE__ 和 __FILE__宏报告的行号和列号
#line 1000 // 把当前行号重置到1000行
#line 10 "cool.h" // 把行号重置为10行,把文件名重置为cool.c
#pragma
告诉编译器自己要怎么做,直接把编译器指令放到源代码中,例如
#pragma c9x on // 使用C9X版本
其他可见pragma预处理命令
基本数据类型和表达式
基本数据类型
输出数据大小
sizeof(int);
占用字节数根据具体实际情况而定
整形
1Byte = 8bit,1字 = 2Byte = 16bit6
类型 |
占用字节 |
数据表示范围 |
int |
4Byte |
-2^31 to 2^31-1 |
unsigned int |
4Byte |
0 to 2^32-1 |
short |
2Byte |
-2^15 to 2^15-1 |
unsigned short |
2Byte |
0 to 2^16-1 |
long |
8Byte |
-2^63 to 2^63-1 |
unsigned long |
8Byte |
0 to 2^64-1 |
无符号和有符号
有符号整型是常规的数值表示范围,无符号是什么意思呢?
int n = 1;
// 其实就是把负数部分的数值范围加到正数部分
unsigned int n = 1;
进制转换
进制类型 |
整数19不同进制的表示 |
整数-19不同进制的表示 |
特点 |
十进制(decimal) |
19 |
-19 |
由0~9的数字序列组成,以10为基的数字系统,可以转化成二进制,八进制和十六进制 |
二进制(binary) |
00010011 |
10010011 |
由0,1的数字序列组成,以2为基的数字系统,可以转化成十进制,八进制和十进制 |
八进制(octal) |
023 |
-023 |
由0~7的数字序列组成,以8为基的数字系统,可以转化成二进制,十进制和十六进制 |
十六进制(hexadecimal) |
0x13 |
-0x13 |
由0到9,A到F(或a到f)的序列组成,以16为基的数字系统,可以转化成二进制,八进制和十进制 |
实型
类型 |
占用字节 |
数符 |
阶码 |
尾数数值 |
总位数 |
float |
4Byte |
1 |
8 |
23 |
32 |
double |
8Byte |
1 |
11 |
52 |
64 |
float a = 2.3; // 单精度
double b = 4.5; // 双精度,当创建一个临时浮点数数值的时候,默认是double
为什么计算机中浮点数是这样表示的呢?相信大家都会有这个疑惑
讲解浮点数的构造
我们知道数值是计算机分成定点数和浮点数,浮点数的定义为:小数点的位置不固定,由指数,基数和阶层组成,用科学计数法表示。比如2 x 10^4
浮点数在计算机中用IEEE754二进制浮点数算术标准
其中:移码 = 阶码真值 + 偏置值(此处偏置值为2 ^ (n-1)-1)= 移码 - 偏置值
在IEE7554标准中,用真值+偏置值,在-127,-128会变成全0或者全1,因此真值的正常范围是-126~127
TIP:对于单精度浮点数是有一个隐藏的最高位1,在小数点前显示,如0.11 = 1.1 * 2^(-1)
字符型
类型 |
占用字节 |
数据表示范围 |
char |
1Byte |
-2^7 to 2^7-1 |
wchar_t |
2Byte |
-2^15 to 2^15-1 |
wchar_t宽字符
在计算机刚发明的时候,字符使用的是ASCII码,表示只能是英文
而随着其他语言的加入,26个字母显然不太符合,因此根据中文,Unicode码便出现了。
而对应的,一个字符占用一个字节的情况变成了一个字符占用两个字节
#include <iostream>
int main() {
char str[] = "asb";
// 末尾自动添加一个'\0',占4个字节
std::cout << sizeof(str) << std::endl;
char str2[] = "刘浩asb";
// 占用10个字节,一个中文2个字节,末尾无'\0',不合法
std::cout << sizeof(str2) << std::endl;
wchar_t str3[] = L"刘浩asb";
// 占用12个字节,一个字符两个字节,末尾添加一个'\0'
std::cout << sizeof(str3) << std::endl;
return 0;
}
转义字符
转义字符 |
意义 |
ASCII码值(十进制) |
\a |
响铃,警报(BEL) |
007 |
\b |
退格(BS) ,将当前位置移到前一列 |
008 |
\f |
换页(FF),将当前位置移到下页开头 |
012 |
\n |
换行(LF) ,将当前位置移到下一行开头 |
010 |
\r |
回车(CR) ,将当前位置移到本行开头 |
013 |
\t |
水平制表(HT) (跳到下一个TAB位置) |
009 |
\v |
垂直制表(VT) |
011 |
\ |
代表一个反斜线字符’’’ |
092 |
’ |
代表一个单引号(撇号)字符 |
039 |
" |
代表一个双引号字符 |
034 |
? |
代表一个问号 |
063 |
\0 |
空字符(NULL) |
000 |
\ddd |
1到3位八进制数所代表的任意字符 |
三位八进制 |
\xhh |
1到2位十六进制所代表的任意字符 |
二位十六进制 |
布尔型
类型 |
占用字节 |
数据表示范围 |
bool |
1Byte |
-2^7 to 2^7-1 |
1表示true,0表示false
其他简单数据类型
类型 |
占用字节 |
数据表示范围 |
描述 |
size_t |
4Byte |
0 to 2^32-1 |
unsigned int,它是 sizeof 关键字的结果 |
ptrdiff_t |
4Byte |
0 to 2^32-1 |
int,它是两个指针相减的结果 |
intptr_t |
4Byte |
0 to 2^32-1 |
int |
// vcruntime.h line 192~195
#ifdef _WIN64
typedef unsigned __int64 size_t;
typedef __int64 ptrdiff_t;
typedef __int64 intptr_t;
可移植类型:stdint.h和inttypes.h
C语言提供了很多有用的整数类型,但是,某些类型名在不同系统下的功能不一样,C99新增了两个头文件stdint.h和inttypes.h,以确保C语言的类型在各系统中的功能相同
// inttypes.h line 109~117
//-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
//
// Output Format Specifier Macros
//
//-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
#define PRId8 "hhd" // short short int
#define PRId16 "hd" // short int
#define PRId32 "d" // int
#define PRId64 "lld" // long long int
#include <stdio.h>
#include <inttypes.h>
int main() {
int8_t m = 2; // 8bit,即1bytes
printf("memory: %d\n", sizeof(m)); // 1
return 0;
}
运算符与表达式
算数运算符
运算符 |
术语 |
示例 |
结果 |
+ |
正号 |
+3 |
3 |
- |
负号 |
-3 |
-3 |
+ |
加 |
10 + 5 |
15 |
- |
减 |
10 — 5 |
5 |
* |
乘 |
10 * 5 |
50 |
/ |
除 |
10 / 5 |
2 |
% |
取模(取余) |
10 % 3 |
1 |
++ |
前置递增 |
a=2;b=++a; |
a=3;b=3; |
++ |
后置递增 |
a=2;b=a++; |
a=3;b=2; |
– |
前置递减 |
a=2;b=–a; |
a=1;b=1; |
– |
后置递减 |
a=2;b=a–; |
a=1;b=2; |
前置递增和后置递增
前置递增:先增后赋值
int a = 1;
int b = ++a; // b = 2
后置递增:先赋值后递增
int a = 1;
int b = a++; // b = 1
前置递增是直接先递增后赋值,而后置递增是先进行赋值,而后开辟了一个新的内存来存放新的增加的数据,因此对于内存占用而言,前置递增比后置递增更具高效。
赋值运算符
运算符 |
术语 |
示例 |
结果 |
= |
赋值 |
a=2;b=3; |
a=2;b=3; |
+= |
加等于 |
a=0;a+=2; |
a=2; |
-= |
减等于 |
a=0;a-=2; |
a=2; |
*= |
乘等于 |
a=0;a*=2; |
a=4; |
/= |
除等于 |
a=0;a/=2; |
a=2; |
%= |
模等于 |
a=0;a%=2; |
a=1; |
比较运算符
提示:根据前面布尔类型可知,'0’代表false,'1’代表true
运算符 |
术语 |
示例 |
结果 |
== |
相等于 |
4==3 |
0 |
!= |
不等于 |
4!=3 |
1 |
< |
小于 |
4<3 |
0 |
> |
大于 |
4>3 |
1 |
<= |
小于等于 |
4<=3 |
0 |
>= |
大于等于 |
4>=1 |
1 |
逻辑运算符
作用:用于根据表达式的值返回真值或假值
运算符 |
术语 |
示例 |
结果 |
! |
非 |
!a |
如果a为假,则!a为真;如果a为真,则!a为假。 |
&& |
与 |
a && b |
如果a和b都为真,则结果为真,否则为假。 |
|| |
或 |
a || b |
如果a和b有一个为真,则结果为真,二者都为假的时候,结果为假 |
位运算
按位取反
按位取反(bitwise NOT)运算,也就是将每一位是1的变成0,是0的变成1。这一运算的运算符为~:
按位与
按位与(bitwise AND)运算,在每一位上对0和1进行逻辑与运算。这一运算的运算符为&:只有两个数的二进制同时为1,结果才为1,否则为0
按位或
按位或(bitwise OR)运算,在每一位上对0和1进行逻辑或运算。这一运算的运算符为|:参加运算的两个数只要两个数中的一个为1,结果就为1
#include <stdio.h>
int main() {
int a;
int b;
a = 1;
b = 2;
printf("%d\n", a & b); // 0
printf("%d\n", a | b); // 3
return 0;
}
移位
- 左移:由于一块分配好的内存位数是有限的,左移运算发生时,这些内存中的最左位会被舍弃,最右位会被补0。
- 右移:相应的,右移运算发生时,这些内存中的最右位会被舍弃,最左位会被补0。
异或运算
a^b的位运算规则是:当且仅当被运算两位上数不同时该位运算结果为1,否则该位运算结果为0。
类型转换
C的强制类型转换
数据类型排名(从高到低):long double,double,float,unsigned long long int,long long int,unsigned long int,long int,unsigned int,int
两个规则
/*
规则 1:char、short 和 unsigned short 值自动升级为 int 值。细心的读者可能已经注意到,char、short 和 unsigned short 都未出现在表 1 中,这是因为无论何时在数学表达式中使用这些数据类型的值,它们都将自动升级为 int 类型。
规则 2:当运算符使用不同数据类型的两个值时,较低排名的值将被升级为较高排名值的类型。
规则 3:当表达式的最终值分配给变量时,它将被转换为该变量的数据类型。
规则 4:当变量值的数据类型更改时,它不会影响变量本身
*/
C的强制类型转换是把变量从一种类型转换为另一种数据类型
// (type_name) expression
// type_name (expression)
#include <stdio.h>
main() {
int sum = 17, count = 5;
double mean;
mean = (double) sum / count;
printf("Value of mean : %f\n", mean );
}
// output:Value of mean : 3.400000
CPP的强制类型转换
static_cast 静态转换
static_cast静态转换相当于C语言中的强制转换,但不能实现普通指针数据(空指针除外)的强制转换,一般用于父类和子类指针、引用间的相互转换。
①用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换。不管是否发生多态,父子之间互转时,编译器都不会报错。
进行上行转换(把派生类的指针或引用转换成基类表示)是安全的;
进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的,但是编译器不会报错。
②用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性也要开发人员来保证。
③把空指针转换成目标类型的空指针。
④把任何指针类型转换成空指针类型。
⑤可以对普通数据的const和non_const进行转换,但不能对普通数据取地址后的指针进行const添加和消去。
⑥无继承关系的自定义类型,不可转换,不支持类间交叉转换。
注意:static_cast不能转换掉expression的const、volatile、或者__unaligned属性
dynamic_cast 动态转换
动态转换的类型和操作数必须是完整类类型或空指针、空引用,说人话就是说,只能用于类间转换,支持类间交叉转换,不能操作普通数据。
主要用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换,
①进行上行转换(把派生类的指针或引用转换成基类表示)是安全的,允许转换;
②进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的,不允许转化,编译器会报错;
③发生多态时,允许互相转换。
④无继承关系的类之间也可以相互转换,类之间的交叉转换。
⑤如果dynamic_cast语句的转换目标是指针类型并且失败了,则结果为0。如果转换目标是引用类型并且失败了,则dynamic_cast运算符将抛出一个std::bad_cast异常
const_cast 常量转换
const_cast,用于修改类型的const或volatile属性,不能对非指针或非引用的变量添加或移除const。
const int g = 20;
//int h = const_cast<int>(g); //不允许对普通数据进行操作
int *h = const_cast<int*>(&g);//去掉const常量const属性
const int g0 = 20;
const int &g2 = g0;
int &h = const_cast<int &>(g0);//去掉const引用const属性
int &h2 = const_cast<int &>(g2);//去掉const引用const属性
const char *g1 = "hello";
char *h = const_cast<char *>(g1);//去掉const指针const属性
reinterpret_cast 重新解释转换
最鸡肋的转换函数,可以将任意类型转换为任意类型,因此非常不安全。只有将转换后的类型值转换回到其原始类型,这样才是正确使用reinterpret_cast方式
static_cast和reinterpret_cast的区别主要在于多重继承
前两个的输出值是相同的,最后一个则会在原基础上偏移4个字节(可查看virtual虚函数博客详述),这是因为static_cast计算了父子类指针转换的偏移量,并将之转换到正确的地址(c里面有m_a,m_b,转换为B*指针后指到m_b处),而reinterpret_cast却不会做这一层转换
字符串和格式化输入/输出
字符串
字符串是一个或多个字符的序列。每一个字符串都有一个\0字符,用他来标记字符串的结束,空字符不是数字0,其ASCII值是0,这意味着数组的容量必须是至少比待定字符串中的字符数多1.
char str[] = {"Hello Wprld"};
字符串和字符的区别
char p = 'x';
sizeof(p); // 1
char o[] = "x";
sizeof(o); // 2,末尾有一个\0
string.h头文件
strlen()函数
获取字符串的长度
char str[] = "abc";
strlen(str); // 3,不计算\0
strcmp()函数
比较字符串的大小
char str1[] = "H";
char str2[] = "h";
strcmp(str1, str2); // 1
strcat()函数
拼接字符串
char str1[] = "H";
char str2[] = "h";
strcat(str1, str2); // str1:Hh
其他函数可见头文件
常量和C预处理
define
#define NAME 1
编译程序时,程序中所有的NAME都会被替换成1,这一过程称为编译时替换,是在预处理的时候进行的。
const
C90标准新增了const关键字,用语限定一个变量可读
const int MON = 12;
明示常量:limits.h和float.h
在limits.h
// limits.h line 32~34
#define SHRT_MIN (-32768) // short最小值
#define SHRT_MAX 32767 // short最大值
#define USHRT_MAX 0xffff // unsigned short最大值
在float.h
// float.h line 71~75
#define DBL_DECIMAL_DIG 17 // # of decimal digits of rounding precision
#define DBL_DIG 15 // # of decimal digits of precision
#define DBL_EPSILON 2.2204460492503131e-016 // smallest such that 1.0+DBL_EPSILON != 1.0
#define DBL_HAS_SUBNORM 1 // type does support subnormal numbers
#define DBL_MANT_DIG 53 // # of bits in mantissa
printf()和scanf()
在stdio.h头文件中
缓冲区大小为512
#define BUFSIZ 512
printf()
取出数据到输出设备中,遇到空格终止输出
符号 |
描述 |
%d |
用于有符号整数 |
%u |
用于无符号整数 |
%c |
用于字符 |
%s |
用于字符串 |
%f |
用于浮点数 |
%e |
用于科学计数法表示浮点数 |
%g |
用于实数 |
%p |
用于内存地址 |
printf()有返回值
_Check_return_opt_
_CRT_STDIO_INLINE int __CRTDECL printf(
_In_z_ _Printf_format_string_ char const* const _Format,
...)
#if defined _NO_CRT_STDIO_INLINE
;
#else
{
int _Result;
va_list _ArgList;
__crt_va_start(_ArgList, _Format);
_Result = _vfprintf_l(stdout, _Format, NULL, _ArgList);
__crt_va_end(_ArgList);
return _Result;
}
#endif
我也看不懂,只能看到他的返回值有int类型,因此用一用就知道了嘛
#include <stdio.h>
int main() {
int n = printf("Hello Wprld");
return 0;
}
// output: 11
所以他输出的是printf的字符串的长度哦!
sprintf()
C 库函数 int sprintf(char *str, const char *format, …) 发送格式化输出到 str 所指向的字符串。
int sprintf(char *str, const char *format, ...)
#include <stdio.h>
int main() {
char information[100];
char *name = "Li, Lei";
char *gender = "male";
int age = 14;
float height = 187.5f;
// 请在这里使用 sprintf
sprintf(information, "%s is a %s. He is %d-year-old and %fcm tall.", name, gender, age, height);
printf("%s", information);
}
其他可见stdio.h头文件
scanf()
输入数据到缓冲区,跳过空格,制表符和换行符
符号 |
描述 |
%d |
用于有符号整数 |
%u |
用于无符号整数 |
%c |
用于字符 |
%s |
用于字符串 |
%f |
用于浮点数 |
%e |
用于科学计数法表示浮点数 |
%g |
用于实数 |
%p |
用于内存地址 |
scanf()的返回值
- scanf()函数返回成功读取的项数
- 当scanf()检测到“文件结尾”时,会返回EOF;如果是从键盘输入,EOF即ctrl+z
#define EOF (-1)
int a;
scanf("%d", &a);
字符输入/输出和输入验证
getchar()和putchar()
他们每次只处理一个字符,读取任何字符
#include <stdio>
int main() {
char ch;
while (ch = getchar() != '#') {
putchar(ch);
}
return 0;
}
缓冲区
大部分系统在用户按下Enter键之前不会重复打印刚输入的字符,这种输入形式成为缓冲输入。用户输入的字符被收集并存储在一个被称为缓冲区(buffer)的临时存储区,按下Enter键之后,程序才可使用用户输入的字符
// stdio.h
// C中定义缓冲区的大小为512个字节
#define BUFSIZ 512
缓冲区分类
完全缓冲区:当缓冲区被填满时才被刷新缓冲区,通常出现在文件输入中。
行缓冲区:出现换行符的时候刷新缓冲区
gets()和puts()
fgets:C 库函数 char *gets(char *str) 从标准输入 stdin 读取一行,并把它存储在 str 所指向的字符串中。当读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定。
char *gets(char *str)
fputs:C 库函数 int puts(const char *str) 把一个字符串写入到标准输出 stdout,直到空字符,但不包括空字符。换行符会被追加到输出中。
int puts(const char *str)
cin 和 cout
C++依赖于C++的I/O解决方案,而不是C语言的I/O解决方案,前者是在头文件iostream和fstream中定义一组类
- streambuf类为缓冲区提供了内存,并提高了用于填充缓冲区、访问缓冲区内容、刷新缓冲区和管理缓冲区内存的类方法
- ios_base类表示流的一般特征,如是否刻度,是二进制还是文本流等等
- ios类基于ios_base,其中包括了一个指向streambuf对象的指针成员
- ostream类是从ios类派生而来的,提供了输出方法
- istream类是从ios类派生而来的,提供了输入方法
- iostream类是基于istream和ostream类的,因此继承了输入方法和输出方法
重定义I/O:为成为国际语言,C++语言必须能够处理需要16位的国际字符集或更宽的字符类型,因此ANSI开发了一套I/O模板basic_ostream<charT, traits< charT>>和basic_istream<charT, traits< charT>>,traits< charT>是一个模板类,位字符类型定义了具体特性,如比较字符是否相等以及字符的EOF值等等
使用cout进行输出
重载的<<运算符
operator cout << data
put()和write()方法
put():现实字符
write():显示字符串
#include <iostream>
using namespace std;
int main() {
char arr[] = "Hello World";
cout.put(arr[0]); // cout << arr[0]
cout.write(arr, 2); // cout << arr[0] << arr[1]
return 0;
}
刷新缓冲区
如果程序使用cout将字节发送给标准输出,由于缓冲,此时不会立即发送到目标地址,而是被存储在缓冲区中,直到缓冲区填满,然后,程序将刷新(flush)缓冲区,把内容发送出去,并情况缓冲区,以存储新的数据。
对于屏幕输出来说,程序不必等到缓冲区被填满。例如,将换行符发送到缓冲区后,将刷新缓冲区。另外,多数C++实现都会在输入即将发生时刷新缓冲区。
cout << "PL;";
float num;
cin >> num;
程序期待输入这一事实,将导致他立刻显示cout消息(即刷新PL;消息),即使输出字符串中没有换行符。如果没有这种特性,程序将等待输入,而无法通过cout消息来提示用户
如果实现不能在所希望时进行刷新输出,可以使用两个控制符中的一个来强行进行刷新:
- flush刷新缓冲区
- endl刷新缓冲区,并插入一个换行符
TIP:ends插入一个空字符
cout << "PL;" << flush;
cout << "Hello World" << endl;
用cout进行格式化
修改计数系统
#include <iostream>
using namespace std;
int main() {
int n = 10;
cout << hex; // 十六进制显示
cout << n << endl;
cout << dec; // 十进制显示
cout << n << endl;
cout << oct; // 八进制显示
cout << n << endl;
return 0;
}
调整字符宽度
可以使用width成员函数将长度不同的数字放到宽度相同的字段中
#include <iostream>
using namespace std;
int main() {
int n = 10;
cout.width(10); // 字符宽度为10
cout << hex; // 十六进制显示
cout << n << endl;
cout.width(10);
cout << dec; // 十进制显示
cout << n << endl;
cout.width(10);
cout << oct; // 八进制显示
cout << n << endl;
return 0;
}
填充字符
当调整字符宽度有多余的宽度时,默认是用空格填充,当然,我们自己可以指定
#include <iostream>
using namespace std;
int main() {
int n = 10;
cout.fill('*');
cout.width(10);
cout << hex; // 十六进制显示
cout << n << endl;
cout.width(10);
cout << dec; // 十进制显示
cout << n << endl;
cout.width(10);
cout << oct; // 八进制显示
cout << n << endl;
return 0;
}
/*
*********a
********10
********12
*/
设置浮点数的显示精度
浮点数精度的函数取决于输出模式,在默认模式下,他指的是显示的总位数
#include <stdio.h>
#include <iostream>
using namespace std;
int main() {
float m = 3.45;
cout.precision(2);
cout << m << endl; // 3.5
cout.precision(1);
cout << m << endl; // 3
return 0;
}
打印末尾的0和小数点
#include <stdio.h>
#include <iostream>
using namespace std;
int main() {
float m = 3.45;
cout.precision(2);
cout << m << endl; // 3.5
cout.precision(4);
cout.setf(ios_base::showpoint);
cout << m << endl; // 3.450
return 0;
}
setf()
setf()函数有两个原型
fmtflags setf(fmtflags);
常量 |
含义 |
ios_base::boolalpha |
输入和输出bool值,可以为true或false |
ios_base::showbase |
对于输出,使用C++基数前缀 |
ios_base::showpoint |
显示末尾的小数点 |
ios_base::uppercase |
对于16进制输出,使用大写字母,E表示法 |
ios_base::showpos |
在正数前面加+ |
fmtflags setf(fmtflags, fmtflags);
不做介绍
使用cin进行输入
cin如何检查输入
- 跳过空格、换行符和制表符,直到遇到非空白字符
- 读取从非空字符开始,到与目标类型不匹配的第一个字符之间的全部内容
#include <iostream>
using namespace std;
int main() {
int m;
cin >> m;
cout << m << endl;
char s;
cin >> s;
cout << s << endl;
return 0;
}
// 123Z
// 123
// Z
流状态
成员 |
描述 |
eofbit |
如果到达文件尾,则设置为1 |
badbit |
如果流被破坏,则设置为1;例如,文件读取错误 |
failbit |
如果输入操作未能读取预期的字符或输出操作没有写入预期的字符,则设置为 1 |
goodbit |
另一种表示为0的方法 |
good() |
如果流可以使用,则返回为true |
eof() |
如果eofbit被设置。则返回true |
bad() |
如果badbit被设置。则返回true |
fail() |
如果failbit被设置。则返回true |
rdstate() |
返回流状态 |
exceptions() |
返回一个位掩码,指出哪些标记导致异常被引发 |
clear(iostate s) |
将流状态设置位s |
setstate(iostate s) |
调用clear() |
sync() |
清空流 |
#include <iostream>
#include <ios>
using namespace std;
int main() {
int m;
cin >> m;
if (cin.fail()) {
cin.clear(ios_base::goodbit);
cin.sync(); // 清空流
}
return 0;
}
其他istream类方法
get(char&)和get(void)
get(char&)
读取下一个字符,即使该字符是空格、制表符或换行符
cin.get(ch);
get(void)
还读取空白,但使用返回值来将输入传递给程序
ch = cin.get();
特征 |
cin.get(ch) |
ch = cin.get() |
传输输入字符的方法 |
赋给参数ch |
将函数返回值赋给ch |
字符传入时函数的返回值 |
指向istream对象的引用 |
字符编码 |
达到文件尾函数的返回值 |
转换成false |
EOF |
getline()、get()
char line[50];
cin.get(line);
cin.get(line, 50);
cin.getline(line);
cin.getline(line, 50);
getline()和get()区别:get()读取换行符,getline不读取换行符
程序流程控制
分类:顺序结构,选择结构,循环结构
- 顺序结构:程序按顺序执行,不发生跳转;
- 选择结构:依据条件是否满足,有选择的执行响应功能;
- 循环结构:依据条件是否满足,循环多次执行某段代码;
选择结构
if语句
作用:执行满足条件的语句
/*
if语句的三种格式:
单行格式if语句:if(条件){条件满足执行的语句};
多行格式if语句:if(条件){条件满足执行的语句}else{条件不满足执行的语句};
多条件的if语句:if(条件1){条件1满足执行的语句}else if(条件2){条件2满足执行的语句}...else{都不满足执行的语句}
*/
switch语句
作用:执行多条件分支语句
switch("表达式")
{
case "结果1":执行语句;break;
case "结果2":执行语句;break;
case "结果3":执行语句;break;
...
default:执行语句;break;
}
循环结构
while循环语句
作用:满足循环条件,执行循环语句
语法:while("循环条件"){"循环语句"}
解释:只要循环条件的结果为真,就执行循环语句
int num = 1;
while(num < 5){
cout << "输出的数字为:" << endl;
num++;
}
do…while循环语句
作用:满足循环条件,执行循环语句
语法:do{"循环语句"} while("循环条件"){"循环语句"};
carefully:与while的区别在于do…while会先执行一次循环语句,再判断循环条件
int main() {
int num = 0;
do{
cout << num << endl;
num++;
}
while(num < 10){
system("pause")
}
}
for循环语句
作用:满足循环条件,执行循环语句
语法:for(起始表达式; 条件表达式; 末尾循环体){循环语句;}
int main() {
for (int i = 0; i < 10; i++){
cout << i << endl;
}
system("pause");
return 0;
}
跳转语句
break语句
作用:用于跳出选择结构或者循环结构
break使用动机:
- 出现在switch条件语句中,作用是终止case并跳出switch;
- 出现在循环语句中,作用是跳出当前的循环语句;
- 出现在嵌套循环中,跳出最近的内层循环语句;
continue语句
作用:在循环语句中,跳过本次循环中余下尚未执行的代码,继续执行下一次循环
int main() {
for (int i = 1; i < 10; i++){
if (i % 2 == 0){
continue;
}
cout << i << endl;
}
system("pause");
return 0;
}
goto语句
作用:可以无条件跳转循环
语法:goto 标记
解释:如果标记的名称存在,执行到goto语句,会跳转到标记的位置
int main() {
cout << "1" << endl;
goto FLAG;
cout << "2" << endl;
cout << "3" << endl;
cout << "4" << endl;
FLAG;
cout << "5" << endl;
system("pause");
return 0;
}
数组、函数和结构体
数组
数组是用一定顺序关系描述的若干对象集合体,组成数组的对象称为该数组的元素,数组元素用数组名与带括号的下标表示,同一数组的各元素具有相同的数据类型。
内存上数组是一串连续的空间
一维数组
可知,数组名表示的是数组首元素的地址
int arr[] = {1, 2, 4, 5};
// 空缺的部分用\0代替
int arr[20] = {3, 2, 4, 6};
/*
1. 可以统计整个数组在内存中的长度:sizeof(arr)
获取数组长度:(1). 数据类型是字符串:strlen();
(2). 其他:sizeof(arr)/sizeof(arr[0])
末尾元素的下标:sizeof(arr)/sizeof(arr[0]) - 1
2. 可以获取数组在内存中的首地址(原本输出的是十六进制,int转化成十进制来查看):(int*)arr或者arr转为 printf("%d", arr);
或printf("%I64d", arr);
3. 可以看数组中第一个元素的地址:(int*)&arr[0]或者&arr[0]转为同上
*/
测试代码
#include <iostream>
#include <inttypes.h>
#include <stdint.h>
#include <limits.h>
#include <float.h>
#include <stdio.h>
using namespace std;
int main() {
int arr[] = {2, 3, 5};
// 数组名即首元素地址
cout << arr << endl;
// 解引用,2
cout << *arr << endl;
// 获得第二个元素的地址
cout << &(*(arr+1)) << endl;
// 获得第二个元素的值
cout << *(arr+1) << endl;
return 0;
}
二维数组
int arr[10][11] = { 0 };
int arr[][] = {
{1, 2, 3},
{3, 4, 5}
};
int arr[2][2] = {
{1, 2},
{4, 3}
}
/*
1. 查看二维数组所占内存空间:sizeof(arr)
2. 查看二维数组第一行所占内存空间:sizeof(arr[0])
3. 查看二维数组第一个元素所占内存空间:sizeof(arr[0][0])
4. 获取二维数组的首地址:int(arr)
5. 可以看数组中第一行元素的地址:(int)&arr[0]
6. 可查看数组中第一个元素的地址:(int)&arr[0][0]
*/
二维数组的其他表示方法
int *p[3]; // 表示一个含有三个int型指针的数组
int (*p)[3]; // 表示一个含有三个元素的指针
字符数组
char str[] = "Hello World";
sizeof(str); // 12,末尾有一个\0
函数
作用:将一段经常使用的代码封装起来,减少重复代码,一个较大的程序,一般分为若干个程序块,每个模块实现特有的功能
函数的定义
函数的定义:返回值类型;函数名;参数表列;函数体语句;return表达式
TIP:如果只是单纯的函数定义,是不占任何内存的,只有当函数调用的时候才会占用内存
语法:
返回值类型 函数名 (参数列表){
函数名语句
return 表达式
}
函数的调用
功能:使用定义好的函数
TIP:函数调用的时候,会将函数以及函数的参数和函数的返回值进入栈区,占用内存。
语法:函数名(参数)
// 函数定义
int add(int num1, int num2){ // 定义中的num1,num2成为参数形式,简称形参
int sum = num1 + num2;
return sum;
}
int main() {
int a = 10;
int b = 10;
// 调用add函数
int sum = add(a, b); // 调用时的a,b成为实际参数,简称实参
cout << sum << endl;
system("pause");
return 0;
}
函数的分类
函数的常见样式:无参无返,无参有返,有参无返,有参有返
#include<iostream>
using namespace std;
// 函数的声明
int max(int a, int b);
int max(int a, int b){
return a > b ? a : b
}
int main() {
int a = 1;
int b = 2;
cout << max(a, b) << endl;
system("pause");
return 0;
}
函数作为参数传参
double func(int m, int n) {}
void print((double)(*func)(int m, int n)) {}
函数重载
函数的参数类型不同,或参数个数不同,或二者兼得,这叫做函数重载
void func(int a, int b) {}
void func(int c) {}
内联函数
函数调用的时候,会消耗很大的内存,因此为了解决这一矛盾,出现了内联函数,通过将函数体内的代码直接插入函数调用处来节省调用函数的时间开销,以空间换时间的方案,目的是提高函数的执行效率。
要求:
- 不可含有if、switch语句或循环语句
- 函数体不宜过大,一般不超过5行
inline void print() {}
lambda匿名函数
Lambda表达式(也叫lambda函数,或简称lambda),是从C++ 11开始引入并不断完善的,是能够捕获作用域中变量的匿名函数对象。
捕获列表 []
捕获列表是零或多个捕获符的逗号分隔符列表,可选地以默认捕获符开始(仅有的默认捕获符是 & 和 = )。默认情况下,从lambda生成的类都包含一个对应该lambda所捕获变量的数据成员。类似任何普通类地数据成员,lambda的数据成员也在lambda对象创建时被初始化。类似参数传递,变量的捕获方式也可以是值或引用。
// 值捕获
void func()
{
int i = 100;//局部变量
//将i拷贝到明位f的可调用对象
auto f = [i] { return i; };
i = 0;
int j = f(); //j=100,因为i是创建时拷贝的
}
// 引用捕获
void func()
{
int i = 100;//局部变量
//对象f包含i的引用
auto f = [&i] { return i; };
i = 0;
int j = f(); //j=0,传递的是引用
}
[ ]。空捕获列表,lambda不能使用所在函数中的变量。
[=]。函数体内可以使用Lambda所在作用范围内所有可见的局部变量(包括Lambda所在类的this),并且是值传递方式(相当于编译器自动为我们按值传递了所有局部变量)。
[&]。函数体内可以使用Lambda所在作用范围内所有可见的局部变量(包括Lambda所在类的this),并且是引用传递方式(相当于编译器自动为我们按引用传递了所有局部变量)。
[this]。函数体内可以使用Lambda所在类中的成员变量。
[a]。将a按值进行传递。按值进行传递时,函数体内不能修改传递进来的a的拷贝,因为默认情况下函数是const的。要修改传递进来的a的拷贝,可以添加mutable修饰符。
[&a]。将a按引用进行传递。
[=,&a, &b]。除a和b按引用进行传递外,其他参数都按值进行传递。
[&, a, b]。除a和b按值进行传递外,其他参数都按引用进行传递。
形参列表 ()
lambda形参列表和一般的函数形参列表类似,但不允许默认实参(C++14 前)。当以 auto 为形参类型时,该 lambda 为泛型 lambda(C++14 起)。与一个普通函数调用类似,调用一个lambda时给定的实参被用来初始化lambda的形参。
//代码在VS2019中测试
void func()
{
int i = 1, j = 2;
auto f = [](int a,int &b) {
a = 10;
b = 20;
//输出:10 20
std::cout << a << " " << b << std::endl;
};
f(i,j);
//输出:1 20
std::cout << i << " " << j << std::endl;
}
返回类型 ->
当我们需要为一个lambda定义返回类型时,需要使用尾置返回类型。返回类型若缺省,则根据函数体中的 return 语句进行推断(如果有多条return语句,需要保证类型一直,否则编译器无法自动推断)。默认情况下,如果一个lambda函数体不包含return语句,则编译器假定返回void。
void func()
{
auto f = []() ->double {
if (1 > 2)
return 1;
else
return 2.0;
};
std::cout << f() << std::endl;
}
函数体 {}
略,同普通函数的函数体。
说明符
mutable:允许 函数体 修改各个复制捕获的对象,以及调用其非 const 成员函数;
constexpr:显式指定函数调用运算符为 constexpr 函数。此说明符不存在时,若函数调用运算符恰好满足针对 constexpr 函数的所有要求,则它也会是 constexpr; (C++17 起)
consteval:指定函数调用运算符为立即函数。不能同时使用 consteval 和 constexpr。(C++20 起)
默认情况下,对于一个值被拷贝的变量,lambda不会改变其值。假如我们希望能改变一个被捕获的变量的值,就必须在参数列表后面加上关键字mutable。而一个引用捕获的变量则不受此限制。
//代码在VS2019中测试
void func()
{
int i = 10, j = 10;
//加上mutable才可以在lambda函数中改变捕获的变量值
auto f = [i, &j]() mutable {
i = 100, j = 100;
};
i = 0, j = 0;
f();
//输出:0 100
std::cout << i << " " << j << std::endl;
}
结构体
定义:结构体属于用户自定义的数据类型,允许用户存储不同的数据类型
结构体的定义和使用
语法:struct 结构体名{结构体成员列表};
通过结构体创建变量方式有三种:
struct 结构体名 变量名;
struct 结构体名 变量名 = {成员1值,成员2值}
- 定义结构体时顺便创建变量
// 结构体定义
struct Student{
// 成员列表
string name; // 姓名
int age; // 年龄
int score; //分数
}
int main() {
// struct 结构体名 变量名;
struct Student s1;
// 给s1属性赋值,通过访问结构体变量中的属性
s1.name = "张三";
s1.age = 13;
s1.score = 99;
//struct 结构体名 变量名 = {成员1值,成员2值}
struct Student s2 = {"张三", 23, 98};
/*
定义结构体时顺便创建变量
struct Student{
// 成员列表
string name; // 姓名
int age; // 年龄
int score; //分数
}s3;
*/
s3.name = "张三";
s3.age = 13;
s3.score = 99;
system("pause");
return 0;
}
typedef重命名
typedef struct LNode {
...
}LNode;
结构体的分类
结构体数组:静态创建
作用:将自定义的结构体放入到数组中方便维护
语法:struct 结构体名 数组名[元素个数] = {{}, {}, {}, {}...{}}
// 结构体定义
struct Student{
// 成员列表
string name; // 姓名
int age; // 年龄
int score; //分数
}
int main() {
// 结构体数组
struct Student arr[3] = {
{"张三", 13, 34},
{"李四", 23, 45},
{"王五", 12, 45}
}
// 给结构体数组的元素赋值
arr[2].name = "赵六";
arr[2].age = 34;
arr[2].score = 78;
// 遍历结构体数组
for(i = 0;i <3; i++){
cout << arr[i].name;
cout << arr[i].age;
cout << arr[i].score;
cout << endl;
}
}
结构体指针:动态创建
作用:通过指针访问结构体中的成员
语法:利用操作符->可以通过结构体访问结构体属性
// 结构体定义
struct Student{
// 成员列表
string name; // 姓名
int age; // 年龄
int score; //分数
}
int main() {
// 创建学生结构体变量
struct Student stu = {"张三", 12, 34};
// Student stu = {"张三", 12, 34}; // 结构体作为一个数据类型,也可以写成数据类型 变量 = ...格式
// 通过指针指向结构体变量
struct Student * p = &stu;
// Student * p = &stu;
// 通过指针访问结构体变量中的数据
cout << p -> age;
cout << p -> name;
cout << p -> socre;
cout << endl;
}
结构体的妙用
结构体嵌套结构体
作用:结构体中的成员可以是另外第一个结构体
例如:每个老师辅导一个学员,一个老师的结构体中,记录一个学生的结构体
#include<iostream>
using namspace std;
#include<string>
// 结构体定义
struct Student {
// 成员列表
string name; // 姓名
int age; // 年龄
int score; //分数
}
struct Teacher {
// 成员列表
int id; // 编号
string name ; // 姓名
int age; //分数
struct Student stu; // 辅导的学生
}
int main() {
// 结构体嵌套结构体
struct Teacher t;
t.id = 1000;
t.name = "不圆";
t.age = 99;
t.stu.name = "八戒";
t.stu.age = 45;
t.stu.score = 56;
cout << t.id;
cout << t.name;
cout << t.age;
cout << t.stu.name;
cout << t.stu.age;
cout << t.stu.score;
}
结构体做函数参数
作用:将结构体作为参数向函数中传递
传递方式:值传递,地址传递
#include<iostream>
using namspace std;
#include<string>
// 结构体定义
struct Student {
// 成员列表
string name; // 姓名
int age; // 年龄
int score; //分数
}
// 值传递,形参发生了改变不会影响实参
void printStudent(struct Student stu){ // 或者(Student stu)
cout << stu.name;
cout << stu.age;
cout << stu.score;
cout << endl;
}
int main() {
struct Student stu;
stu.name = "张三";
stu.age = 21;
stu.score = 89;
printStudent(stu)
}
// 地址传递,形参发生了改变会影响实参
void printStudent(struct Student *p){
cout << p->name << endl;
}
int main() {
struct Student stu;
stu.name = "张三";
stu.age = 21;
stu.score = 89;
printStudent(&stu)
}
结构体中const的使用场景
作用:用const防止误操作
#include<iostream>
using namspace std;
#include<string>
// 结构体定义
struct student {
// 成员列表
string name; // 姓名
int age; // 年龄
int score; //分数
}
// 将形参改成指针,可以减少内存空间,而且不会复制出新的副本出来
void printstudent(const struct student *s) {
// s -> = 150 加入const后不可修改
cout << s->name;
cout << s->age;
cout << s->score;
cout << endl;
}
int main() {
// 创建结构体变量
struct student s = {"张三", 23, 34};
printstudent(&s);
system("pause");
return 0;
}
结构体数据顺序对内存影响
可见结构体内存偏移量
enum枚举类型
枚举是 C 语言中的一种基本数据类型,它可以让数据更简洁,更易读
enum 枚举名 {枚举元素1,枚举元素2,……};
enum DAY
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
};
TIP:第一个枚举成员的默认值为整型的 0,后续枚举成员的值在前一个成员上加 1。我们在这个实例中把第一个枚举成员的值定义为 1,第二个就为 2,以此类推。
union共用体
为了定义共用体,您必须使用 union 语句,方式与定义结构类似。union 语句定义了一个新的数据类型,带有多个成员。union 语句的格式如下:
union [union tag]
{
member definition;
member definition;
...
member definition;
} [one or more union variables];
结构体、枚举类型、共用体占用内存
结构体占用内存:按照结构体内存偏移量原则,最大字节对齐原则
struct A { // 16
int a; // 4
short b; // 2
int c; // 4
char d; // 1
};
枚举类型占用内存:默认为int类型,枚举类型的尺寸不能够超过int类型的尺寸
共用体占用内存:占用内存最大的元素对齐原则,union的大小为最长类型的整数倍
即其大小必须满足两个条件:
- 大小足够容纳最宽的成员;
- 大小能被其包含的所有基本数据类型的大小所整除
union st { // 4 + 8 + 1 = 13,对齐8,则为16
int n; // 4
double g; // 8
char a[9]; // 1
};
指针、引用和内存分区模型
指针
作用:可以通过指针间接访问内存,提高代码运行的效率。
- 内存编号是从0开始记录的,一般用十六进制表示;
- 可以利用指针变量保存地址;
#include <iostream>
using namespace std;
int main() {
int a = 1;
int* p = &a;
cout << p << endl; // 0x61fe14
}
指针变量的定义和使用
语法:数据类型 * 变量名;
int main() {
// 1. 定义指针
int a = 1;
// 2. 指针定义的语法:数据类型 * 指针变量名
int * p;
// 3. 让指针记录变量a的地址
p = &a;
cout << "a的地址为" << &a << endl;
cout << "a的地址为" << p << endl;
// 4. 使用指针
// 可以通过解引用的方式来找到指针指向的内存
// 指针前加一个*代表解引用找到数据并修改内存
*p = 1000;
cout << a <<endl;
cout << p <<endl;
system("pause");
return 0;
}
指针的关系运算
#include<iostream>
using namespace std;
// 判断一个数是否是回文数
int main() {
char s[71], *p;
get(s); // 标准库函数get()与cin类似甚至更好
top = &s[0]; // top值表示的是s变量的首地址
end = &s[sizeof(s)-1]; // end表示的是s变量的最后一个字节的地址,此处还可用&s[strlen(s)-1]
for(top, end;top < end; top++, end--) { // 此处end是高地址,top是低地址
if(*top != *end) { break; }
}
if(top >= end) {
cout << "是一个回文数" << endl;
}else {
cout << "不是回文数" << endl;
}
system("pause");
return 0;
}
/*
指针的相减和赋值运算
如果指针p1指向数组a里面的第二个元素a[1],指针p2指向数组a里面的第五个元素a[4],那么p1 - p2 = 3(表示的是两指针所指变量之间相距的同类型变量的个数)
指针没有相加运算,没有任何意义
*/
指针所占内存空间
- 在32位操作系统下,占用4个字节;
- 在64位操作系统下,占用8个字节;
#include <iostream>
using namespace std;
int main() {
int a = 1;
int* p = &a;
cout << sizeof(p) << endl; // 8
}
空指针和野指针
空指针
指针变量指针内存中编号为0的空间;
用途:初始化指针变量;
注意:空指针指向的内存是不可以访问的;
int main() {
// 指针变量指针内存中编号为0的空间
int * p = NULL;
// 访问空指针报错
// 内存编号0~255位系统所占内存,不允许用户访问
// *p = 100;
cout << *p << endl;
system("pause");
return 0;
}
野指针
指针变量指向非法的内存空间
int main() {
// 指针变量p指向内存地址中编号为0x1100的空间
int * p = (int *)0x1100;
// 访问野指针报错
cout << *p << endl;
system("pause");
return 0;
}
指针常量和常量指针
const修饰指针:常量指针;
const修饰常量:指针常量;
const即修饰指针,又修饰常量;
int main() {
int a = 10;
int b = 10;
// const修饰的是指针,指针指向可以改,指针指向的值不可以更改
const int * p1 = &a;
p1 = &b; // 正确
// *p1 = 100; 报错
// const修饰的是常量,指针指向不可以改,指针指向的值可以更改
int * const p2 = &a;
// p2 = &b; 错误
*p2 = 100; // 正确
// const修饰的是常量,指针指向不可以改,指针指向的值可以更改
const int * const p3 = &a;
*p3 = 100; // 错误
p4 = &b; // 错误
}
指针和数组
概念:C++语言中的指针和数组有着密切的关系,数组名可以认为是一个常量指针
作用:利用指针访问数组元素
// 对于一维数组
int main() {
int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 指向数组的指针,此时arr代表的是数组的首地址,数组名可以认为是一个常量指针,因此可以直接写成int * p = arr;
int * p = arr; //这里的arr=&arr[0]
cout << "第一个元素:" << arr[0] << endl;
cout << "指针访问的第一个元素:" << *p << endl;
p++; // 让指针向后偏移4个字节
for(int i = 0; i < 10; i++){
cout << *p << endl;
P++;
}
system("pause");
return 0;
}
// 对于二维数组
int main() {
int a[3][3] = {{1, 3, 5}, {7, 9, 11}, {13, 15, 17}}
int *p = a[0]; // p是一个执行整型变量的指针,他可以指向一般的整型变量,也可以指向数组中的元素
for(p; a[0] < a[0] + 8; p++) {
cout << *p << " ";
cout << endl;
}
}
数组指针
char* str = "Hello";
Hello是一个字面量,str指向的是一个地址,即
char* str = 0xffff;
指针和函数
函数参数
作用:利用指针做函数参数,可以修改实参的值
// 值传递
void max(int a, int b){
int temp = a;
a = b;
b = temp;
}
// 地址传递
void max(int * p1, int * p2){ // 传入的必须是一个地址&a
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
// 针形函数
void *func(int x) { // 返回值必须是一个int型指针
cout << x << endl;
}
int main() {
int a = 1;
int b = 2;
max(&a, &b);
}
函数返回值
int* print() {}
函数自身作为参数
void print((double)(*func)(int m, int n)){}
智能指针
#include <memory>
auto_ptr
C98推出,这个是被摒弃的,为何?
auto_ptr<string> ps (new string("Hello World"));
auto_ptr<string> vocation;
vocation = ps; // 将两个指针指向同一个string对象,这是不可接受的
unique_ptr
C11引入,旨在替代不安全的 auto_ptr,它持有对对象的独有权——两个unique_ptr 不能指向一个对象,即 unique_ptr 不共享它所管理的对象
auto_ptr<string> p1 (new string("Hello World"));
auto_ptr<string> p2;
p1 = p2;
其中p2接管了string对象的所有权后,p1的所有权被剥夺,可是两个指针会同时删除一份数据
unique_ptr<string> p1 (new string("Hello World"));
unique_ptr<string> p2;
p1 = p2;
此时编译器认为p1=p2非法,因此unique_ptr更安全
除此之外,还将delete[]和new[]配对,模板auto_ptr使用delete而不是delete[]
unique_ptr<int[]> p (new int[3]{1,2,3});
p[0] = 0;// 重载了operator[]
shared_ptr
基于引用计数的智能指针,如果程序要使用多个指针指向同一对象的指针,应该使用shared_ptr。
//
另一个特点是如果可以安全的放入STL容器中。
#include <vector>
std::vector<shared_ptr<string>> v;
weak_ptr
不做讨论,配合shared_ptr使用
引用
引用的基本使用:
作用:给变量起别名
语法:数据类型 &别名 = 原名
int a = 10;
int &b = a;
引用的注意事项
- 引用必须初始化
- 引用在初始化后,不可以改变
引用做函数参数
作用:函数传参时,可以利用引用的技术让形参修饰实参
优点:可以简化指针修改实参
// 2. 地址传递
void he(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
// 3. 引用传递
int my(int &a, int &b) {
int temp = a;
a = b;
b = temp;
}
引用函数做返回值
作用:引用是可以作为函数的返回值存在的
注意:不要返回局部变量引用
用法:函数调用作为左值
// 返回局部变量引用,不可用
int& test01() {
int a = 10; // 局部变量在四区中的栈区
return a;
}
// 返回静态变量引用
int& test02() { // int& 函数名表示以引用的方式返回,其他如int* 函数名表示以指针的方式返回
static int a = 10;
return a;
}
int main() {
int &ref = test01();
cout << ref << endl;
system("pause");
return 0;
}
引用的本质:在C++内部实现一个指针常量
常量引用
作用:常量引用主要用来修饰形参,防止误操作
在函数列表中,可以加const修饰形参,防止形参改变实参
// 引用使用的场景,通常用来修饰形参
int main() {
// 常量引用
// 使用场景:用来修饰形参,防止误操作
// int a = 10;
// 加上const后编译器将代码修改
const int &b = 10; // 引用必须引一块合法分内存空间
// ref = 10 报错,加上const后不可修改
system("pause");
return 0;
}
void getnum(const int &a) { // int &a表示以引用的方式接收这个值,如果在外面则是以什么样的方式返回
// a = 100; // 不加const能直接修改
cout << a << endl;
}
int main() {
int a = 10;
getnum(a);
}
指针和引用的区别
- 指针初始化可以指向null
- 引用初始值必须指向一个值
内存分区模型
内存分区
C++程序在执行时,将内存大方向划分为4个区域
- 代码区:存放函数的二进制文件,由操作系统进行管理的;
- 全局区:存放全局变量和静态变量以及常量;
- 栈区:由编译器自动分配释放,存放函数的参数值,局部变量等;
- 堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统完成;
意义:不同区域存放的数据,赋予不同的生命周期,给我们更大的灵活编程
程序运行前:
在程序编译后,生成了exe可执行文件,未执行该程序前分成两个区域
代码区
- 存放CPU执行的机器指令;
- 代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可;
- 代码区是只读的,使其只读的原因是防止程序意外的修改了程序;
全局区
- 全局变量和静态变量存放于此;
- 全局区还包括了常量区,字符串常量和其他常量也存放于此;
- 该区域的数据在程序结束后由操作系统释放;
int a = 0; // 全局变量
int main() {
int b = 1; // 局部变量
static int c = 2; // 静态变量
}
程序运行后:
栈区
- 由编译器自动分配释放,存放函数的参数值,局部变量等;
- 注意事项:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放;
堆区
- 由程序员分配释放,若程序员不释放,程序结束的时由操作系统回收;
- 在C++中主要利用new在栈区开辟内存
void *func() { // 返回为地址要加*
// 利用new关键字将数据开辟到堆区
int *p = new int(10);
return p;
}
int main() {
// 在堆区开辟数据
int *p = func();
cout << *p << endl;
system("pause");
return 0;
}
malloc、calloc和free
// 分配100个int类型的数字指针
// malloc
int* arr = (int*)malloc(sizoef(int), 100);
// calloc
int* arr = (int*)calloc(100, sizeof(int));
// free
free(arr);
arr = NULL;
malloc和calloc区别
- calloc丞数申请的内存空间是经过初始化的,全部被设成了e,而不是像malloc所申请的空间那样都是未经初始化的。
- calloc函数适合为数组申请空间,我们可以将第二个参数设置为数组元素的空间大小,将第-个参数设置为数组的元素数量。
new和delete
C++中利用new操作符在堆区开辟数据
堆区开辟的数据,由程序员手动开辟,手动释放,释放利用操作符delete
语法:new 数据类型;
利用new创建的数据,会返回该数对应的类型的指针
// new的基本语法
int *func(){
// 在堆区创建整型数据
// new返回是 该数据类型的指针
int *a = new int(10);
return a;
}
void test01() {
int *p = func();
cout << *p << endl;
// 堆区的数据,程序员手动开辟,手动释放
// 释放利用操作符delete
delete p;
}
// 在堆区利用new开辟数组
void test02() {
// 创建10整型数据的数组,在堆区;
int * arr = new int[10]; // 10代表数组有十个元素
for(int i = 0;i < 10; i++) {
arr[i] = i + 100
}
for(int i =0 ;i < 10; i++) {
cout << arr[i] << endl;
}
// 释放数组的时候加上[]才可以
delete[] arr;
}
new operator和operator new
以下面代码为例
string *ps = new string("Hello World");
此时的new就是所谓的new operator。这个操作符是语言内建的,他的动作分成两部分
- 分配足够的内存,用来放置某类型的对象
- 调用一个constructor,为刚才分配内存中的那个对象设定初值
我们可以改变的是用来容纳对象的那块内存的分配行为。new operator调用某个函数,执行必要的内存分配动作。
这个函数就是operator new!
void* operator new(size_t size);
其返回值类型是void*,返回一个指针,指向一块内存,未设初值的内存,size_t参数表示需要分配多少内存(size_t是sizeof的返回值)。如下
// 和malloc一样,operator new的唯一任务就是分配内存
void* rawmemory = operator new(sizeof(string));
此时代码为
void* ps = new string("hello world");
编译器会执行
// 取得原始内存,用来放置一个string对象
void* memory = operator new(sizeof(string));
// 将内存中的对象初始化,constructor动作
call string::string("hello world");
// 让ps指向新完成的对象
//static_cast强制类型转换,将void*空指针变成string*类型指针
string* ps = static_cast(memory);
正常情况下,对一个已存在的对象调用constructor没有意义,因为constructor用来将对象初始化,而对象只能被初始化一次。
在某种情况下,偶尔会有一些分配好的内存,需要在上面构建对象,有一个特殊版本的operator new 成为placement new允许这么做。
class widget {
public:
widget(int widgetsize);
...
};
widget* constructwidgetinbuffer(void* buffer, int widgetsize) {
return new(buffer) widget(widgetsize);
}
函数返回指针,指向一个widget object,他被构造于传递给此函数的一块内存缓冲区上。因为这块内存是在函数外声明的,因为当函数释放的时候该内存不会被释放
指定一个额外自变量buffer作为new operator隐式调用operator new时所用,于是,被调用的operator new除了接受"一定得有的size_t自变量之外",还接受了一个void*参数,指向一块内存,准备用来接受构造好的对象,这样的operator new就是所谓的placement new。
这便是placement new。operator new的目的在于为对象找到一块内存,然后返回一个指针指向他,在placement new的情况下,调用者已经知道指向内存的指针了,因为调用者知道对象应该放在那里。
具体可见头文件new.h
剖析delete
为了避免resource leaks(资源泄露),每一个动态分配行为都必须匹配一个相应但相反的动作,函数operator delete对于内建的delete operator,就好像operator new对于new operator一样。
string *ps;
...
delete ps; // delete operator
编译器的行为
- 析构ps所指向的对象
- 释放该对象占用的内存
ps-> ~string(); // 调用对象的dtoroperator
operator delete(ps); // 释放对象所占用的内存
文件输入、输出和异常
C
理解I/O和内核的关系
#include <iostream>
int main() {
std::cout << "Hello World" << std::endl;
return 0;
}
当运行一个程序(软件)的时候,不免要和硬件进行对接协议,这样一个一个对接太麻烦了,因此就产生了内核
这样就软件就可以和内核对接,然后内核再把这些分发给硬件处理协议
内核主要进行内存管理,进程管理,虚拟文件系统,设备光驱
内核具有的权限很大,应用程序具有的权限很小,因此计算机把内存分为了内核态和用户态
他们之间的关系是这样的:
内核程序执行在内核态,用户程序执行在用户态。当应用程序使用系统调用时,会产生一个中断。发生中断后, CPU 会中断当前在执行的用户程序,转而跳转到中断处理程序,也就是开始执行内核程序。内核处理完后,主动触发中断,把 CPU 执行权限交回给用户程序,回到用户态继续工作。
而在进程PCB管理可以看做是一个结构体,里面有一个文件操作符表,表长为1024,从0开始,其中前三个是固定的
- 0:标准输入
- 1:标准输出
- 2:标准错误
当进行cout的时候,系统会产生一个FILE* stdout文件指针,该指针包括三部分:文件描述符,文件定位符以及缓冲区.其中的文件描述符是用来表示哪个文件的,会寄存到PCB的文件描述符表中去。
然后系统向下调用write函数(此时还是用户态)
write(fd, "Hello World", 11);
接着再向下进行调用sys_write(此时进入了内核态)
sys_write("Hello World");
前面说过,内核是硬件和应用程序之间的桥梁,因此,此时进入硬件:光驱,屏幕,网卡。此时进入的是屏幕
Hello World
与文件进行通信
文件分类
文件通常是在磁盘或固态硬盘上的一段已命名的存储区,C提供了两种文件模式:文本模式和二进制模式
I/O的级别
除了选择问及那的模式,大多数情况下,还可以选择I/O的两个级别,底层I/O和标准高级I/O
标准文件
C程序会自动打开3个文件,他们被称为标准输入、标准输出、标准错误输出。
默认打开三个进程,即在文件描述符中会占用0、1、2这三个文件描述符,因此,这三个文件描述符不可用
标准I/O
fopen()函数
FILE *fp; // 创建一个文件描述符
fp = fopen("a.txt", "r");
相关参数可查
getc()和putc()函数
getc:表示从fp指定的文件中获取一个字符
FILE *fp; // 创建一个文件描述符
fp = fopen("a.txt", "r");
char ch;
ch = getc(fp);
while (ch != EOF) {
putchar(ch);
ch = getc(fp);
}
putc:把一个字符放入FILE指针fpout指定的文件中
fclose()函数
关闭fp指定的文件,成功返回0,否则返回EOF
文件I/O
fprintf()和fscanf()函数
fprintf:C 库函数 int fprintf(FILE *stream, const char *format, …) 发送格式化输出到流 stream 中。
int fprintf(FILE *stream, const char *format, ...)
#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE * fp;
fp = fopen ("file.txt", "w+");
fprintf(fp, "%s %s %s %d", "We", "are", "in", 2014);
fclose(fp);
return(0);
}
fscanf:C 库函数 int fscanf(FILE *stream, const char *format, …) 从流 stream 读取格式化输入。
int fscanf(FILE *stream, const char *format, ...)
#include <stdio.h>
#include <stdlib.h>
int main()
{
char str1[10], str2[10], str3[10];
int year;
FILE * fp;
fp = fopen ("file.txt", "w+");
fputs("We are in 2014", fp);
rewind(fp);
fscanf(fp, "%s %s %s %d", str1, str2, str3, &year);
printf("Read String1 |%s|\n", str1 );
printf("Read String2 |%s|\n", str2 );
printf("Read String3 |%s|\n", str3 );
printf("Read Integer |%d|\n", year );
fclose(fp);
return(0);
}
fgets()和fputs()函数
fgets:C 库函数 char *fgets(char *str, int n, FILE *stream) 从指定的流 stream 读取一行,并把它存储在 str 所指向的字符串内。当读取 (n-1) 个字符时,或者读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定。
char *fgets(char *str, int n, FILE *stream)
#include <stdio.h>
int main()
{
FILE *fp;
char str[60];
/* 打开用于读取的文件 */
fp = fopen("file.txt" , "r");
if(fp == NULL) {
perror("打开文件时发生错误");
return(-1);
}
if( fgets (str, 60, fp)!=NULL ) {
/* 向标准输出 stdout 写入内容 */
puts(str);
}
fclose(fp);
return(0);
}
fputs:C 库函数 int fputs(const char *str, FILE *stream) 把字符串写入到指定的流 stream 中,但不包括空字符。
int fputs(const char *str, FILE *stream)
其他标准I/O函数
ungetc(int c,FILE* fp)函数
把字符 char(一个无符号字符)推入到指定的流 stream 中,以便它是下一个被读取到的字符。
int fflush()函数
C 库函数 int fflush(FILE *stream) 刷新流 stream 的输出缓冲区。
fwrite()和fread()函数
perror()
C 库函数 void perror(const char *str) 把一个描述性错误消息输出到标准错误 stderr。首先输出字符串 str,后跟一个冒号,然后是一个空格。
void perror(const char *str)
CPP
程序运行时产生的数据都属于临时数据,程序一旦结束都会被释放,通过文件可以将数据持久化
C++中对文件操作需要包含头文件
文件分类:
- 文本文件:文件以文件的ASCII码形式存储在计算机中
- 二进制文件:文件以文本的二进制形式存储在计算机中,用户一般不能直接读取他们
操作文件的三大类:
- ofstream:写操作;
- ifstream:读操作;
- fstream:读写操作;
写文件
// 写文件步骤如下
// 1. 包含头文件
#include<fstream>
// 2. 创建流对象
ofstream ofs;
// 3. 打开文件
ofs.open("文件路径", 打开方式);
// 4. 写数据
ofs << "写入的数据";
// 5. 关闭文件
ofs.close();
模式标记 |
适用对象 |
作用 |
ios::in |
ifstream fstream |
打开文件用于读取数据。如果文件不存在,则打开出错。 |
ios::out |
ofstream fstream |
打开文件用于写入数据。如果文件不存在,则新建该文件;如果文件原来就存在,则打开时清除原来的内容。 |
ios::app |
ofstream fstream |
打开文件,用于在其尾部添加数据。如果文件不存在,则新建该文件。 |
ios::ate |
ifstream |
打开一个已有的文件,并将文件读指针指向文件末尾(读写指 的概念后面解释)。如果文件不存在,则打开出错。 |
ios:: trunc |
ofstream |
打开文件时会清空内部存储的所有数据,单独使用时与 ios::out 相同。 |
ios::binary |
ifstream ofstream fstream |
以二进制方式打开文件。若不指定此模式,则以文本模式打开。 |
ios::in | ios::out |
fstream |
打开已存在的文件,既可读取其内容,也可向其写入数据。文件刚打开时,原有内容保持不变。如果文件不存在,则打开出错。 |
ios::in | ios::out |
ofstream |
打开已存在的文件,可以向其写入数据。文件刚打开时,原有内容保持不变。如果文件不存在,则打开出错。 |
ios::in | ios::out | ios::trunc |
fstream |
打开文件,既可读取其内容,也可向其写入数据。如果文件本来就存在,则打开时清除原来的内容;如果文件不存在,则新建该文件。 |
注意:文件打开方式可以配合使用,利用|操作符
such as:
-
ios::in | ios::binary
表示用二进制模式,以读取的方式打开文件。 -
ios::out | ios::binary
表示用二进制模式,以写入的方式打开文件。
读文件
// 1. 包含头文件
#include<fstream>
// 2. 创建流对象
ifstream ifs;
// 3. 打开文件并判断文件是否打开成功
ifs.open("文件路径", 打开方式);
// 4. 读数据
// 四种读取方式
// (1).
char buf[1024] = { 0 };
while(ifs >> buf) {
cout << buf << endl;
}
// (2).
char buf[1024] = { 0 };
while(ifs.getline(buf, sizeof(buf))){
cout << buf << endl;
}
// (3).
string ifs;
while(getline(ifs, buf)){
cout << buf << endl;
}
// (4).
char c;
while((c = ifs.get()) != EOF) {
// EOF:the end of file
cout << c;
}
// 5. 关闭文件
ifs.close();
对二进制文件进行写入
二进制方式写文件主要利用流对象调用成员函数write
函数原型:ostream& write(const char * buffer, int len);
参数解释:字符指针buffer指向内存中一段存储空间,len是读写的字节数。
对二进制文件进行读取
二进制方式读文件主要利用流对象调用成员函数read
函数原型:ostream& read(char * buffer, int len);
参数解释:字符指针buffer指向内存中一段存储空间,len是读写的字节数
异常
exit()和abort()
二者差异:
exit()函数结束程序,返回一个值给操作系统,告知程序的最后状态。在调用exit()函数之后,控制权会移交给操作系统。 在结束程序之前,exit()函数会调用之前使用atexit()注册过的所有函数,按照LIFO次序调用,关闭所有打开的文件,删除tmpfile()函数建立的所有临时文件。
abort()函数会发出一个SIGABRT信号来终止程序的执行。不会调用之前用atexit()函数注册的清理函数。至于会不会清除缓冲区,这个与系统相关。如以下实现则清空了。但没有关闭。进程结束它自己会关闭。
#include <iostream>
#include <stdlib.h>
int heanm(int m, int n);
int heanm(int m, int n) {
if (m == n) {
std::cout << "the function is a error\n";
abort();
// exit(int status);
}
return m / (m - n);
}
int main() {
int m = 10, n = 10;
return 0;
}
/*
/* output:
/* the function is a error
/* abnormal program termination
*/
异常机制
抛出异常:throw
if (b == 0) {
throw "i like you~";
}
捕获异常
// 捕获固定异常
try {
// 要监测的代码段,其中必须有抛出一个错误选择的异常
} catch(const char* error) { // arguement要和抛出异常的类型相匹配
cout << error << endl;
}
// 捕获任意异常
try {
// 监测代码段
} catch(...) { // 通用性较强
cout << "error" << endl;
}
代码
int test01(int m, int b) {
if (b == 0) throw "fool";
return m/b;
}
int main() {
try {
int ret = test01(10, 0);
std::cout << ret << std::endl;
}
catch (int e) {
std::cerr << e << std::endl;
}
catch (float e) {
std::cerr << e << std::endl;
}
catch (const char* e) {
std::cout << e << std::endl;
}
return 0;
}
// 输出:fool
栈解退
C++通常是将函数信息放在栈中的,当被调用的函数执行完毕后,程序将使用该地址来确定从哪里开始继续执行程序,另外,函数调用将函数参数放在栈中,在栈中,这些变量被视为自动变量,当一个函数中调用了其他函数,以此类推,当函数结束时,程序流程将调到该函数被调用时的存储的地址处,同时栈顶元素被释放,以此类推,每个函数都在释放其自动变量,如果自动变量是类对象,则类的析构函数将被调用。
exception类
stdexcept异常类
该头文件定义了几个异常类,首先,logic_error和runtime_error类,他们都是以公有方式从exception派生而来的
#include <stdexcept>
class logic_error: public exception {
public:
explicit logic_error(const string& what_arg);
...
};
class domain_error: public exception {
public:
explicit domain_error(const string& what_arg);
...
};
// 每个类都有一个类似于logic_error的构造函数,提供一个供方法what()返回的字符串
std::exception |
该异常是所有标准 C++ 异常的父类。 |
std::bad_alloc |
该异常可以通过 new 抛出。 |
std::bad_cast |
该异常可以通过 dynamic_cast 抛出。 |
std::bad_exception |
这在处理 C++ 程序中无法预期的异常时非常有用。 |
std::bad_typeid |
该异常可以通过 typeid 抛出。 |
std::logic_error |
理论上可以通过读取代码来检测到的异常。 |
std::domain_error |
当使用了一个无效的数学域时,会抛出该异常。 |
std::invalid_argument |
当使用了无效的参数时,会抛出该异常。 |
std::length_error |
当创建了太长的 std::string 时,会抛出该异常。 |
std::out_of_range |
该异常可以通过方法抛出,例如 std::vector 和 std::bitset<>::operator。 |
std::runtime_error |
理论上不可以通过读取代码来检测到的异常。 |
std::overflow_error |
当发生数学上溢时,会抛出该异常。 |
std::range_error |
当尝试存储超出范围的值时,会抛出该异常。 |
std::underflow_error |
当发生数学下溢时,会抛出该异常。 |
try {
...
} catch(out_of_bounds& oe) {
...// catch out_of_bounds error
} catch(logic_error& oe) {
...// catch remaining logic_error family
} catch(exception& oe) {
...// catch runtime_error, exception obects
}
bad_alloc和new
对于new而导致的内存分配问题,C++的最新处理方式是让new引发bad_alloc异常
#include <new>
#include <iostream>
#include <cstdlib>
using namespace std;
struct Big {
double stuff[20000];
};
int main() {
Big *pb;
try {
cout << "Trying to get a big block of memory\n";
pb = new Big[10000];
cout << "Got past the new request:\n";
} catch(bad_alloc &ba) {
cout << "Get the exception!\n";
cout << ba.what() << endl;
exit(EXIT_FAILURE);
}
cout << "Menory successfully allocated\n";
pb[0].stuff[0] = 4;
cout << pb[0].stuff[0] << endl;
delete [] pb;
return 0;
}
如果内存不足的话则返回
/*
/*Trying to get a big block of memory
/*Get the exception!
/*std::bad_alloc
*/
空指针和new
很多代码都是在new在是失败时返回空指针时编码的,为处理new的变化,有些编译器提供了一个标记(开关),然后用户选择所需的行为
int *p = new (std::nothrow) int;
int *pp = new (std::nothrow) int[500];
// 可将上一个例子的代码改为
Big *pb;
pb = new (std::nothrow) Big[10000];
if (pb == 0) {
cout << "Could not allocate memory, Bye\n" << endl;
exit(EXIT_FAILURE);
}
异常的接口声明
前面对于一个函数是
void func() {} // 此时可以抛出任何异常
优化
void func() throw (int, char, char*) {} // 指定抛出异常
即
// 此时他只能抛出int, char, char*类型的异常
void test01() throw(int, char, char*) {
throw 10;
}
int main() {
try {
test01();
}
catch(int e) {
std::cerr << e << std::endl;
}
return 0;
}
这些是动态异常规范,但是现在用的很少,我们现在一般都是直接定义为无异常抛出,以便编译器更好的执行代码
void func() onexcept {}
类和对象
C++面向对象三大特性:封装,继承,多态
C++认为万事万物皆可为对象,对象上有其属性和行为
封装
封装的意义:
- 将属性和行为作为一个整体,表现生活中的事物;
- 将属性和行为加以权限控制;
语法:class 类名{访问权限:属性 / 行为};
// 设计一个圆类,求圆的周长
#include<iostream>
using namespace std;
const double PI = 3.14;
class Circle{
// 访问权限
public:
// 属性
int m_r;
// 行为
// 获取圆的周长
double calculateZC() {
return 2 * PI * m_r;
};
};
int main() {
// 通过圆类创建一个具体的圆
Circle cl;
// 给圆对象的属性进行赋值
cl.m_r = 10;
cout << "周长为" << cl.calculateZC() << endl;
system("pause");
return 0;
}
访问权限
权限分类:
- 公共权限(public):成员类内可以访问,类外可以访问
- 保护权限(protected):成员类内可以访问,类外不可以访问,子类可以访问父类中的保护内容
- 私有权限(private):成员类内可以访问,类外不可以访问,子类可以不访问父类中的保护内容
class Person {
public:
string m_Car;
protected:
int m_P;
private:
int m_S;
}
在C++中唯一的区别就在于默认的访问权限不同
区别:
- struct默认权限为公共;
- class默认权限为私有;
- 生活中我们买的电子产品都基本会有出厂设置,在某一天我们不用的时候也会删除自己信息数据保证安全;
- C++的面向对象来源于生活,每个对象也都会有初始化设置以及对象销毁前清理数据的设置;
构造函数和析构函数
对象的初始化和清理也是两个非常重要的安全问题
- 一个对象或者变量没有初始状态,对其使用后结果是未知
- 同样的使用完一个对象或变量,没有及时清理,也会造成一定的安全问题
C++利用了构造函数和析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作,对象的初始化和清理工作是编译器强制我们做的事情,因此如果我们不提供构造和析构,编译器会提供编译器提供的构造函数和析构函数是空实现。
- 构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用
- 析构函数:主要作用在于对象销毁前自动调用,执行一些清理工作
构造函数语法:类名(){}
- 构造函数,没有返回值也不写void
- 函数名称与类名相同
- 构造函数可以有参数,因此可以发生重载
- 程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次
析构函数语法:~类名(){}
- 析构函数,没有返回值也不写void
- 函数名称与类名相同,在名称前加上符号~
- 析构函数不可以有参数,因此不可以发生重载
- 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
#include<iostream>
using namespace std;
class Person {
public:
// 构造函数:用于初始化类
Person() {
cout << "sd" << endl;
}
// 析构函数
~Person() {
cout << "sad" << endl;
}
/*
~Cat() {
if (m_Name != NULL) {
delete m_Name;
m_Name = NULL;
}
}
*/
};
void test01() {
Person p; // 对象的初始化操作
}
int main() {
test01();
system("pause");
return 0;
}
构造函数的分类及调用
构造函数分类
无参构造
class Person {
public:
// 无参构造
Person() {
cout << "a" << endl;
}
有参构造
class Person {
public:
// 有参构造
Person(int a) {
age = a;
cout << "a" << endl;
}
单参数构造:explicit关键字
在C++的类型转换中,继承了C的强大的隐式转换的功能,但是这样也有极大的缺点
正常情况下是short > long,char > int,但是在隐式转换下,他有可能变成char > long,short > int。
这在类的单参数构造函数中表现的尤为明显!
构造函数一般有两种功能:构造函数和隐式类型转换
class student {
public:
int age;
student(int m): age(m) {}
}
int main(void) {
student s(10); // 1 正常写法 通过
student s1 = 10; // 2 通过
}
第二种情况下我们可以知道10是一个int整型变量,可是用“=”号就变成了
student s;
student s1(20);
int n = 10;
s = s1; // 这样是一个拷贝
s = n; // 这样就把一个int转换成了一个student对象
从类对象转换成int变量同理
所谓的隐式类型转换,是有一个奇怪的member function,关键词operator之后加上一个类型名称。
我们可以将它定义为:
class stu {
public:
// 此处不能为此函数指定返回值类型,因为其返回值类型基本上已经表现欲函数名称上
operator int() const;
}
int main(void) {
stu r(1, 2);
int d = 5 * r;
return 0;
}
可知在int d = 5 * r中可以将int作为一个运算符重载,因此此时类对象变成了一个int型变量
用explicit的意义
TIP:只能写在类声明中,不能写在类定义中
class student {
public:
int age;
explicit student(int m): age(m) {}
}
int main(void) {
student s(10); // 1 正常写法,通过
// student s1 = 10; // 2 错误,不通过
}
这种情况下就不允许进行这种int到类对象的隐式转换。(类的构造函数默认是implicit的隐式转换)
拷贝构造
class Person {
public:
// 拷贝(copy)构造
Person(const Person &p) {
// 将传入的人身上所有属性(age)拷贝到我身上
age = p.age;
}
移动构造
C11最新引入,其实就是转移所有权
移动构造函数中的参数类型,&&符号表示是右值引用;即将消亡的值就是右值,函数返回的临时变量也是右值,这样的单个的这样的引用可以绑定到左值的,而这个引用它可以绑定到即将消亡的对象,绑定到右值.
class Person {
public:
// 拷贝(copy)构造
Person(const Person &&p) : age(p.age){
p.age = nullptr;
}
三种调用方式
- 括号法
- 显示法
- 隐式转换法
#include<iostream>
using namespace std;
// 1. 构造参数的分类和调用
/* 按照参数分类:无参构造(默认构造)和有参构造
按照类型分类:普通构造和拷贝构造
*/
class Person {
public:
// 无参构造
Person() {
cout << "a" << endl;
}
// 有参构造
Person(int a) {
age = a;
cout << "a" << endl;
}
// 拷贝(copy)构造
Person(const Person &p) {
// 将传入的人身上所有属性拷贝到我身上
age = p.age;
}
~Person() {
cout << "b" << endl;
}
int age;
};
// 调用
void test01() {
// 括号法
Person p1; // 默认构造函数
Person p2(10); // 有参构造
Person p3(p2); // 拷贝(有参)构造
// 显示法
Person p1; // 默认构造
Person p2 = Person(10); // 有参构造
Person p3 = Person(p2); // 拷贝构造
// 隐式转换法
Person p4 = 10; // Person p4 = Person(10);
Person p5 = p4; // 拷贝构造
};
int main() {
system("pause");
return 0;
}
拷贝构造函数调用时机
- 使用一个已经创建完毕的对象来初始化一个对象
- 值传递的方式给函数参数传值
- 以值方式返回局部对象
#include<iostream>
using namespace std;
// 拷贝构造函数调用时机
class Person {
public:
// 构造函数
Person() {
cout << "a" << endl;
}
Person(int age) {
m_age = age;
cout << "a" << endl;
}
Person(const Person& p) {
m_age = p.m_age;
cout << "a" << endl;
}
// 析构函数
~Person() {
cout << "b" << endl;
}
int m_age;
};
// 1. 使用一个已经创建完毕的对象来初始化一个对象
void test01() {
Person p1(20);
Person p2(p1);
}
// 2. 值传递的方式给函数参数传值
void dowe(Person p) {
}
void test02() {
Person p;
dowe(p);
}
// 3. 以值方式返回局部对象
void dowe2() {
Person p1;
return p1;
}
void test03() {
Person p = dowe2(); // 此时的p不是dowe2传递的p1,地址不同但是值相同
}
int main() {
test01();
test02();
test03();
system("pause");
return 0;
}
构造函数调用规则
默认情况下,C++编译器至少给一个类添加3个函数
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对属性进行拷贝
构造函数调用规则:
- 如果用户有定义构造函数,C++不在提供默认无参狗砸,但是会提供默认拷贝构造
- 如果用户定义拷贝构造函数,C++不会提供其他构造函数
***后续完善:深拷贝和浅拷贝
- 浅拷贝:简单的赋值拷贝操作;
- 深拷贝:在堆区重新申请空间,进行拷贝操作;
初始化列表
作用:C++提供了初始化列表语法,用来初始化属性
语法:构造函数():属性1(值1), 属性2(值2),...{}
#include<iostream>
using namespace std;
// 初始化列表
class Person {
public:
// 传统的初始化操作
Person(int a, int b, int c) {
m_A = a;
m_B = b;
m_C = c;
}
// 初始化列表操作初始化属性
Person(int a, int b, int c) :m_A(a), m_B(b), m_C(c) {
}
int m_A;
int m_B;
int m_C;
};
void test01() {
Person p(10, 20, 30);
cout << p.m_A;
cout << p.m_B;
cout << p.m_C;
cout << endl;
}
int main() {
test01();
system("pause");
return 0;
}
类成员作为类对象
C++类中中的成员可以是另一个类的对象,我们称该成员为对象成员
such as
class A {}
class B {
A a; // B类中有对象A作为成员,A为对象成员
}
#include<iostream>
using namespace std;
#include<string>
// 手机类
class Phone {
public:
Phone(string m_name) {
m_name = m_name;
}
string m_name;
};
// 人类
class Person {
public:
Person(string name, string m_name): name(name), m_Phone(m_name)
{
}
string name;
Phone m_Phone;
};
void test01() {
Person p("张三", "apple");
}
int main() {
test01();
system("pause");
return 0;
}
静态成员static
分类:
- 静态成员变量
- 所有对象共享一份数据
- 在编译阶段分配内存(内存分配在全局变量区)
- 类内声明,类外初始化
int Person::m_A = 0; // 静态成员变量,类外初始化
- 静态成员函数
- 所有对象共享一个函数
- 静态成员函数只能访问静态成员变量
#include<iostream>
using namespace std;
#include<string>
class Person {
public:
// 静态成员函数
static void func() {
m_A = 100; // 静态成员函数可以访问静态成员变量
// m_B = 200,报错,静态成员函数不可以访问非静态成员变量
cout << "sad" << endl;
}
static int m_A; // 静态成员变量,类内声明
};
int Person::m_A = 0; // 静态成员变量,类外初始化
// 有两种访问方式
void test01() {
// 1. 通过对象访问
Person p;
p.func();
// 2. 通过类名访问
Person::func();
}
int main() {
test01();
system("pause");
return 0;
}
static使用要求
那么静态成员函数有特点呢?
- 静态成员之间可以相互访问,包括静态成员函数访问静态数据成员和访问静态成员函数;
- 非静态成员函数可以任意地访问静态成员函数和静态数据成员;
- 静态成员函数不能访问非静态成员函数和非静态数据成员;
- 调用静态成员函数,可以用成员访问操作符(.)和(->)为一个类的对象或指向类对象的指针调用静态成员函数,也可以用类名::函数名调用(因为他本来就是属于类的,用类名调用很正常)
this指针概念
成员变量和成员函数是分开存储的,每一个非静态函数只会诞生一份函数实例,也就是说多个同类型的对象会公园一块代码,那么问题是:这一块代码是如何区分哪个对象调用自己的呢?
- this指针指向被调用的成员函数所属的对象;
- this指针是隐含每一个非静态成员函数内的一种指针;
- this指针不需要定义,直接使用即可;
this指针的用途:
当形参和成员变量同名时,可以用this指针来区分;
在类的非静态成员函数中返回对象本身,可使用return *this
#include <iostream>
using namespace std;
class Person {
public:
Person(int age) {
// this指针指向被调用的成员函数所属的对象
this ->age = age;
// 当形参和成员变量同名时,可以用this指针来区分
// 此时this ->age表示的是成员变量int age
}
int age;
};
// 解决名称冲突
void test01() {
Person p1(18);
cout << p1.age << endl;
}
// 返回对象本身用*this
void test02() {
Person p1(10);
Person p2(10);
cout << p1.age << endl;
}
int main()
{
test01();
test02();
cout << "Hello World!" << endl;
}
空指针访问成员函数
// 成员变量默认前面加了个this
const修饰成员函数
常函数:
- 成员函数加const后我们称这个函数为常函数;
- 常函数内不可以修改成员属性;
- 成员属性声明加关键字mutable后,在常函数中依旧可以改变;
常对象:
- 声明对象前加const称该对象为常对象;
- 常对象只能调用常函数;
友元
TIP:友元是单向的
目的:让一个函数或者类访问另一个类中私有成员0
关键字:friend
友元实现方式:
- 全局函数做友元
#include <iostream>
using namespace std;
#include<string>
// 建筑物类
class Building {
// goodgay全局函数是building的好朋友,可以访问building私有成员
friend void goodgay(Building* building);
public:
Building() {
m_sittingroom = "客厅";
m_bedroom = "卧室";
}
public:
string m_sittingroom; // 客厅
private:
string m_bedroom; // 卧室
};
// 全局函数
void giidgay(Building *building) {
cout << building->m_sittingroom << endl;
}
void test01() {
Building building;
giidgay(&building);
}
int main() {
system("pause");
return 0;
}
- 类做友元
- 成员函数做友元
运算符重载
概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型
一元运算符重载
#include <iostream>
using namespace std;
class Distance
{
private:
int feet; // 0 到无穷
int inches; // 0 到 12
public:
// 所需的构造函数
Distance(){
feet = 0;
inches = 0;
}
Distance(int f, int i){
feet = f;
inches = i;
}
// 显示距离的方法
void displayDistance()
{
cout << "F: " << feet << " I:" << inches <<endl;
}
// 重载负运算符( - )
Distance operator- ()
{
feet = -feet;
inches = -inches;
return Distance(feet, inches);
}
};
int main()
{
Distance D1(11, 10), D2(-5, 11);
-D1; // 取相反数
D1.displayDistance(); // 距离 D1
-D2; // 取相反数
D2.displayDistance(); // 距离 D2
return 0;
}
二元运算符重载
二元运算符需要两个参数,下面是二元运算符的实例。我们平常使用的加运算符( + )、减运算符( - )、乘运算符( * )和除运算符( / )都属于二元运算符。就像加(+)运算符。
下面的实例演示了如何重载加运算符( + )。类似地,您也可以尝试重载减运算符( - )和除运算符( / )。
#include <iostream>
using namespace std;
class Box
{
double length; // 长度
double breadth; // 宽度
double height; // 高度
public:
double getVolume(void)
{
return length * breadth * height;
}
void setLength( double len )
{
length = len;
}
void setBreadth( double bre )
{
breadth = bre;
}
void setHeight( double hei )
{
height = hei;
}
// 重载 + 运算符,用于把两个 Box 对象相加
Box operator+(const Box& b)
{
Box box;
box.length = this->length + b.length;
box.breadth = this->breadth + b.breadth;
box.height = this->height + b.height;
return box;
}
};
// 程序的主函数
int main( )
{
Box Box1; // 声明 Box1,类型为 Box
Box Box2; // 声明 Box2,类型为 Box
Box Box3; // 声明 Box3,类型为 Box
double volume = 0.0; // 把体积存储在该变量中
// Box1 详述
Box1.setLength(6.0);
Box1.setBreadth(7.0);
Box1.setHeight(5.0);
// Box2 详述
Box2.setLength(12.0);
Box2.setBreadth(13.0);
Box2.setHeight(10.0);
// Box1 的体积
volume = Box1.getVolume();
cout << "Volume of Box1 : " << volume <<endl;
// Box2 的体积
volume = Box2.getVolume();
cout << "Volume of Box2 : " << volume <<endl;
// 把两个对象相加,得到 Box3
Box3 = Box1 + Box2;
// Box3 的体积
volume = Box3.getVolume();
cout << "Volume of Box3 : " << volume <<endl;
return 0;
}
关系运算符重载
C++ 语言支持各种关系运算符( < 、 > 、 <= 、 >= 、 == 等等),它们可用于比较 C++ 内置的数据类型。
您可以重载任何一个关系运算符,重载后的关系运算符可用于比较类的对象。
下面的实例演示了如何重载 < 运算符,类似地,您也可以尝试重载其他的关系运算符。
#include <iostream>
using namespace std;
class Distance
{
private:
int feet; // 0 到无穷
int inches; // 0 到 12
public:
// 所需的构造函数
Distance(){
feet = 0;
inches = 0;
}
Distance(int f, int i){
feet = f;
inches = i;
}
// 显示距离的方法
void displayDistance()
{
cout << "F: " << feet << " I:" << inches <<endl;
}
// 重载负运算符( - )
Distance operator- ()
{
feet = -feet;
inches = -inches;
return Distance(feet, inches);
}
// 重载小于运算符( < )
bool operator <(const Distance& d)
{
if(feet < d.feet)
{
return true;
}
if(feet == d.feet && inches < d.inches)
{
return true;
}
return false;
}
};
int main()
{
Distance D1(11, 10), D2(5, 11);
if( D1 < D2 )
{
cout << "D1 is less than D2 " << endl;
}
else
{
cout << "D2 is less than D1 " << endl;
}
return 0;
}
赋值运算符重载
就像其他运算符一样,您可以重载赋值运算符( = ),用于创建一个对象,比如拷贝构造函数。
下面的实例演示了如何重载赋值运算符。
#include <iostream>
using namespace std;
class Distance
{
private:
int feet; // 0 到无穷
int inches; // 0 到 12
public:
// 所需的构造函数
Distance(){
feet = 0;
inches = 0;
}
Distance(int f, int i){
feet = f;
inches = i;
}
void operator=(const Distance &D )
{
feet = D.feet;
inches = D.inches;
}
// 显示距离的方法
void displayDistance()
{
cout << "F: " << feet << " I:" << inches << endl;
}
};
int main()
{
Distance D1(11, 10), D2(5, 11);
cout << "First Distance : ";
D1.displayDistance();
cout << "Second Distance :";
D2.displayDistance();
// 使用赋值运算符
D1 = D2;
cout << "First Distance :";
D1.displayDistance();
return 0;
}
++ 和 – 运算符重载
递增运算符( ++ )和递减运算符( – )是 C++ 语言中两个重要的一元运算符。
下面的实例演示了如何重载递增运算符( ++ ),包括前缀和后缀两种用法。类似地,您也可以尝试重载递减运算符( – )。
#include <iostream>
using namespace std;
class Time
{
private:
int hours; // 0 到 23
int minutes; // 0 到 59
public:
// 所需的构造函数
Time(){
hours = 0;
minutes = 0;
}
Time(int h, int m){
hours = h;
minutes = m;
}
// 显示时间的方法
void displayTime()
{
cout << "H: " << hours << " M:" << minutes <<endl;
}
// 重载前缀递增运算符( ++ )
Time operator++ ()
{
++minutes; // 对象加 1
if(minutes >= 60)
{
++hours;
minutes -= 60;
}
return Time(hours, minutes);
}
// 重载后缀递增运算符( ++ )
Time operator++( int )
{
// 保存原始值
Time T(hours, minutes);
// 对象加 1
++minutes;
if(minutes >= 60)
{
++hours;
minutes -= 60;
}
// 返回旧的原始值
return T;
}
};
int main()
{
Time T1(11, 59), T2(10,40);
++T1; // T1 加 1
T1.displayTime(); // 显示 T1
++T1; // T1 再加 1
T1.displayTime(); // 显示 T1
T2++; // T2 加 1
T2.displayTime(); // 显示 T2
T2++; // T2 再加 1
T2.displayTime(); // 显示 T2
return 0;
}
输入、输出运算符重载
C++ 能够使用流提取运算符 >> 和流插入运算符 << 来输入和输出内置的数据类型。您可以重载流提取运算符和流插入运算符来操作对象等用户自定义的数据类型。
在这里,有一点很重要,我们需要把运算符重载函数声明为类的友元函数,这样我们就能不用创建对象而直接调用函数。
下面的实例演示了如何重载提取运算符 >> 和插入运算符 <<。
#include <iostream>
using namespace std;
class Distance
{
private:
int feet; // 0 到无穷
int inches; // 0 到 12
public:
// 所需的构造函数
Distance(){
feet = 0;
inches = 0;
}
Distance(int f, int i){
feet = f;
inches = i;
}
friend ostream &operator<<( ostream &output,
const Distance &D )
{
output << "F : " << D.feet << " I : " << D.inches;
return output;
}
friend istream &operator>>( istream &input, Distance &D )
{
input >> D.feet >> D.inches;
return input;
}
};
int main()
{
Distance D1(11, 10), D2(5, 11), D3;
cout << "Enter the value of object : " << endl;
cin >> D3;
cout << "First Distance : " << D1 << endl;
cout << "Second Distance :" << D2 << endl;
cout << "Third Distance :" << D3 << endl;
return 0;
}
函数调用()运算符重载
函数调用运算符 () 可以被重载用于类的对象。当重载 () 时,您不是创造了一种新的调用函数的方式,相反地,这是创建一个可以传递任意数目参数的运算符函数。
下面的实例演示了如何重载函数调用运算符 ()。
#include <iostream>
using namespace std;
class Distance
{
private:
int feet; // 0 到无穷
int inches; // 0 到 12
public:
// 所需的构造函数
Distance(){
feet = 0;
inches = 0;
}
Distance(int f, int i){
feet = f;
inches = i;
}
// 重载函数调用运算符
Distance operator()(int a, int b, int c)
{
Distance D;
// 进行随机计算
D.feet = a + c + 10;
D.inches = b + c + 100 ;
return D;
}
// 显示距离的方法
void displayDistance()
{
cout << "F: " << feet << " I:" << inches << endl;
}
};
int main()
{
Distance D1(11, 10), D2;
cout << "First Distance : ";
D1.displayDistance();
D2 = D1(10, 10, 10); // invoke operator()
cout << "Second Distance :";
D2.displayDistance();
return 0;
}
不要重载&&、||和,操作符
继承
继承啊面向对象三大特性之一
语法:class 子类(派生类):继承方式 父类(基类)
#include <iostream>
using namespace std;
// 继承实现页面
// 继承的好处,减少重复的代码
class Base {
public:
void header() {
cout << "a" << endl;
}
};
class Java : public Base {
public:
void content() {
cout << "as" << endl;
}
};
int main() {
system("pause");
return 0;
}
继承方式
分类:公共继承,保护继承,私有继承
/*
公共继承:public:private不可继承
保护继承:protected:private不可继承
私有继承:private:
*/
继承中的对象模型:子类继承父类的属性后在内存中依旧可以找到,说明是存在一种实际继承关系,而不是短暂的虚拟的继承关系。
继承中的构造和析构顺序
子类继承父类后,当创建子类对象时,也会调用父类的构造函数,此时是父类和子类的顺序是
父类构造-子类构造-子类析构-父类析构
继承同名成员处理方法
- 访问子类同名成员,直接访问即可
- 访问父类同名函数,需要加作用域
#include<iostream>
using namespace std;
// 继承中同名成员处理
class Base {
public:
Base() {
m_A = 100;
}
void func() {
cout << "asa" << endl;
}
void func(int a) {
cout << "asa" << endl;
}
int m_A;
};
class Son :public Base {
public:
Son() {
m_A = 10;
}
void func() {
cout << "asa" << endl;
}
int m_A;
};
// 同名成员属性
void test01() {
Son s;
cout << s.Base::m_A << endl; // 加一个作用域
}
// 同名成员函数
void test02() {
Son s;
s.Base::func();
// 如果子类中出现和父类同名的成员函数,子类的同名成员会隐藏掉父类的同名成员
s.Base::func(100);
}
int main() {
system("pause");
return 0;
}
继承同名静态成员处理方法
静态成员和非静态成员出现同名,处理方法一致
- 访问子类同名成员,直接访问即可
- 访问父类同名函数,需要加作用域
多继承语法
C++允许一个类继承多个类
语法:
class 子类:继承方式 父类1,继承方式 父类2...
多继承可能会引发父类中有同名成员出现,需要加作用域区分
菱形继承案例(略)
多态
多态是C++面向对象三大特性之一
多态分类
- 静态多态:函数重载和运算符重载属于静态多态,复用函数名;
- 动态多态:派生类和虚函数实现运行时多态;
静态多态和动态多态区别
- 静态多态的函数地址早绑定:编译阶段确定函数地址;
- 动态多态的函数地址晚绑定:运行阶段确定函数地址;
#include<iostream>
using namespace std;
class Animal {
public:
// virtual修饰的函数表示虚函数
virtual void speak() {
cout << "aa" << endl;
}
};
class cat :public Animal {
public:
void speak() {
cout << "asa" << endl;
}
};
// 执行说话的函数
// 地址早绑定 在编译阶段确定函数地址
// 如果想执行cat说话 地址近不能提前绑定
/*
动态多态满足条件
1. 有继承关系
2. 子类重写父类的虚函数
动态多态使用
父类的指针或者引用 执行子类对象
重写:函数有返回值类型 函数名 参数列表 完全一致称为重写
*/
void dospeak(Animal& animal) { // Animal& animal = cat
animal.speak();
}
void test01() {
cat cat;
dospeak(cat);
}
int main() {
system("pause");
return 0;
}
多态的原理剖析
当子类发生重写时,子类中的虚函数表,内部会替换成子类的函数地址
当父类的指针或者引用指针子类对象的时候,发生多态
虚函数
虚函数表
virtual tables:vtbls。一个由函数指针架构而成的数组(某些编译器会以链表取代数组)。程序中每一个class凡声明(或继承)虚函数者,都有自己的一个vtbl,而其中的条目就是该class的各个虚函数实现体的指针
class C1 {
public:
C1();
virtual ~C1();
virtual void f1();
virtual int f2(char c) const;
virtual void f3(const string& s);
void f4() const;
};
注意,非虚函数f4并不在表格中,C1的constructor也一样
如果是C2继承C1,然后重新定义继承而来的虚函数
class C2:public C1 {
public:
C2();
virtual ~C2();
virtual void f1();
virtual void f5(char* str);
...
};
虚函数指针
virtual table pointers:vptrs,凡是有虚函数的class,其对象都含有一个隐藏的data member,用来指向该class的vtbl。(必须在每一个拥有虚函数的对象付出一个额外指针的代价)
vptr主要有以下作用:
根据对象的vptr找出其vtbl,这是一个简单的动作,因为编译器知道对象的哪里去找出pvtr。成本只有一个便宜调整和一个指针间接动作
找出被调函数在vtbl内的对应指针,一个差移以求进入vtbl数组
调用上一个步骤所得指针所指向的函数
当发生继承的时候,派生类会把基类的虚函数指针vptr继承过来,不过此时指向变成了派生类的虚函数表
我们写代码来论证
#include <iostream>
using std::cout;
using std::endl;
class animal {
public:
animal(){}
virtual void showname() {}
virtual void eat() {}
virtual ~animal() {}
};
class cat: public animal {
public:
cat() {}
~cat() {};
virtual void show() {
cout << "cat" << endl;
}
virtual void eat() {
cout << "fish" << endl;
}
};
int main() {
animal* a = new animal();
cat* b = new cat();
cout << sizeof(a) << endl;
cout << sizeof(b) << endl;
return 0;
}
// output
// 8 8
多重继承之虚基类
多重继承往往导致virtual base classes(虚拟基类)的需求。
来看看一个特殊的多重继承:棱形继承
class A {...};
class B:virtual public A {...};
class C:virtual public B {...};
class D:public B, public C {...};
具体可见相关博客
纯虚函数和抽象类
在多态中,通常父类中虚函数的是实现是毫无意义的,主要都是调用子类重写的内容,因此可以将虚函数改成纯虚函数
纯虚函数语法:virtual 返回值类型 函数名 (参数列表) = 0;
当类中有了纯虚函数,这个类也称为抽象类
抽象类特点:
- 无法实例化对象;
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类;
虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码,因此需要将父类中的析构函数改成虚析构或者纯虚析构
虚析构和纯虚析构共性:
- 可以解决父类指针释放子类对象;
- 都需要有具体的函数实现;
虚析构和纯虚析构区别:
- 如果是纯虚析构,该类属于抽象类,无法实例化对象
虚析构语法:virtual ~类名() {}
纯虚析构语法:virtual ~类名() = 0;
#include<iostream>
using namespace std;
#include<string>
class Animal {
public:
// 纯虚函数
virtual void speak() = 0; // 只要是纯虚函数,都是抽象类
};
class Cat :public Animal {
public:
Cat(string name) {
m_Name = new string(name);
}
virtual void speak() {
cout << "asa" << endl;
}
/* virtual ~Animal() // 虚析构
{
cout << "as" << endl;
}*/
/*
virtual ~Animal() = 0 // 纯虚析构
全局函数
Animal ::~Animal()
{
}
*/
~Cat() {
if (m_Name != NULL) {
delete m_Name;
m_Name = NULL;
}
}
string *m_Name;
};
void test01() {
Animal *animal = new Cat("muqier");
animal->speak();
// 父类指针在析构时候,不会调用子类中析构函数,导致子类如果有堆区属性,出现内存泄漏
delete animal;
}
int main() {
system("pause");
return 0;
}
好博客就要一起分享哦!分享海报
此处可发布评论
评论(0)展开评论
展开评论
您可能感兴趣的博客