1 初识C++
一、C++擅长领域
游戏开发
金融科技
实时系统
计算机视觉
仿真建模
网络通信
音视频、嵌入式等
推荐书籍
《C++ Primer》 《C++ 程序设计语言》
二、概述
c++文件
后缀名:.cpp/.cc
头文件后缀名:.hh/.hpp
安装g++命令:
sudo apt install g++
编译命令
g++ 文件名.cc/.cpp [-o name]
c++代码预设
cd ~/.vim/plugged/prepare-code/snippet/ vim snippet.cpp
通过vim打开snippet.cpp文件设置代码模板
#include <iostream> using namespace std;void test(){} int main(int argc, char * argv[]){return 0; }
注:C++原生头文件不加 .h 后缀
命名空间
在不同的头文件中用了相同的名字来命名所定义的类或函数,这样在程序中就会出现名字冲突。名字冲突就是在同一个作用域中有两个或多个同名的实体,C语言中避免名字冲突,只能进行起名约定。C++为了解决命名冲突 ,引入了命名空间,所谓命名空间就是一个可以由用户自己定义的作用域,在不同的作用域中可以定义相同名字的变量,互不干扰,系统能够区分它们。
C++中定义命名空间的基本格式如下:
namespace namespaceTest { int val1 = 0; char val2; }
在声明一个命名空间时,大括号内可以存放:变量、常量、函数、结构体、引用、类、对象、模板、命名空间等,它们都称为实体。
命名空间的使用方式
- using编译指令
- 作用域限定符
- using声明机制
using编译指令
如using namespace std,其中std代表的时标准命名空间。cout和endl都是std中的实体,使用了using编译指令后,者两个实体就可以直接使用了。
#include <iostream>//using编译指令 using namespace std;int main(int argc, char * argv[]) {cout << "hello c++" << endl;return 0; }
作用域限定符
使用命名空间实体时,都直接加上作用域限定符。
namespace test {int number = 10;void display() {std::cout << "test's display()" << std::endl;} } int oneChapter() {std::cout << "test's number = " << test::number << std::endl;test::display;return 0; }
优点:准确,只要命名空间有这个实体就能够准确调用。
缺点:繁琐
using声明机制
需要什么就声明什么。
using声明机制的作用域是从using语句开始,到using所在的作用域结束。同样的,建议将using声明语句写在局部作用域中。此时即使命名空间中实体与全局位置实体重名,在局部位置也遵循“就近原则”形成屏蔽。
#include <iostream> using std::cout; using std::endl;int number = 100;namespace wd { int number = 10; void display() {cout << "wd::display()" << endl; } }//end of namespace wdint main(void) {using wd::number;using wd::display;//只写函数名cout << "wd::number = " << number << endl; //ok,访问到wd::numberdisplay();return 0; }
注意事项
using编译指令尽量写在局部作用域,这样using编译指令的效果也会在其作用域结束时结束。但是在同一个作用域下使用两个命名空间中同名的实体,这种方法就不可行了,可以采用命名空间的作用域限定符方式。
采用using编译指令使用命名空间中的实体时,要注意避免命名空间中实体与全局位置实体同名。
在不清楚命名空间中实体的具体情况时,尽量不使用using编译指令。如果一个名称空间中有多个实体,使用using编译指令,就会把该空间中的所有实体一次性引入到程序之中。
对于初学者来说,如果对一个命名空间中的实体并不熟悉时,直接使用这种方式,有可能还是会造成名字冲突的问题,而且出现错误之后,还不好查找错误的原因
在同一作用域内用using声明机制, 不同的命名空间的实体,不能是同名的,否则会发生冲突。
using声明机制的特点是:需要哪个实体的时候就引入到程序中,不需要的实体就不引入,尽可能减小犯错误的概率。
命名空间的嵌套使用
类似于文件夹下还可以建立文件夹,命名空间中还可以定义命名空间。
namespace wd {int num = 100;void func(){cout << "func" << endl;}namespace cpp{int num = 200;void func(){cout << "cpp::func" << endl;}}//end of namespace cpp}//end of namespace wd//方式一,使用作用域限定精确访问实体 void test0(){cout << wd::cpp::num << endl;wd::cpp::func(); }//方式二,using编译指令一次性引入cpp的实体 void test1(){using namespace wd::cpp;cout << num << endl;func(); }//方式三,using声明语句 void test2(){using wd::cpp::num;using wd::cpp::func;cout << num << endl;func(); }
匿名命名空间
命名空间还可以不定义名字,不定义名字的命名空间称为匿名命名空间(简称匿名空间)。
通常,如果我们希望一部分实体只在本文件中起作用,那么可以将它们定义在匿名空间中。
其定义方式如下:
namespace { //... }//end of anonymous namespace
使用匿名空间中实体时,可以直接使用,也可以加上作用域限定符。
namespace {int val = 10;void func(){cout << "func()" << endl;} }//end of anonymous namespace//以下用法均ok void test0(){cout << val << endl;cout << ::val << endl;func();::func(); }
如果匿名空间中定义了和全局位置中同名的实体,会有冲突。
此时如果使用::作用域限定符,也无法访问到匿名空间中重名的实体,只能访问到全局的实体。
匿名空间注意事项:
- 匿名空间不要定义与全局空间中同名的实体;
- 匿名空间中的实体不能跨模块调用。
跨模块调用问题
一个.c/.cc/*.cpp的文件可以称为一个模块。
可以跨模块调用的内容:
- 全局变量和函数
- 有名空间中的实体
全局变量和函数是可以跨模块调用的
模块A.cc
int num = 100;void print(){cout << "print()" << endl; }
模块B.cc
extern int num;//外部引入声明 extern void print(); void test0(){cout << num << endl;print(); }
对externA.cc和externB.cc联合编译,实现跨模块调用
有名命名空间中的实体可以跨模块调用
模块A.cc
namespace wd {int val = 300;void display(){cout << "wd::display()" << endl;} }//end of namespace wd
模块B.cc
namespace wd {int val = 300;void display(){cout << "wd::display()" << endl;} }//end of namespace wd
命名空间中的实体跨模块调用时,要在新的源文件中再次定义同名的命名空间,在其中通过extern引入实体。
进行联合编译时,这两次定义被认为是同一个命名空间。
易错点
如果在externA.cc中有同名的命名空间实体与全局实体,并且同时在externB.cc中做外部引入,直接访问的是全局实体, 通过::访问的是命名空间中实体
模块A.cc
int val = 100; void display(){cout << "display()" << endl; }namespace wd {int val = 300;void display(){cout << "wd::display()" << endl;} }//end of namespace wd
模块B.cc
extern int val; extern void display();namespace wd { extern int val; extern void display(); }//访问到全局的实体 void test0(){cout << val << endl;display(); }//访问到命名空间中的实体 void test1(){cout << wd::val << endl;wd::display(); }void test2(){using namespace wd;cout << val << endl; //冲突display(); //冲突 }using wd::val;//声明冲突 using wd::display;//声明冲突 void test3(){//... }void test4(){using wd::val;using wd::display;cout << val << endl; //ok,在本作用域中对全局的实体起到了屏蔽的效果display(); //ok }
使用规则:如果需要跨模块调用命名空间中的实体,要尽量避免它们与全局位置的实体重名,在使用时尽量采取作用域限定的方式 。
静态变量和函数只能在本模块内部使用
匿名空间的实体只能在本模块内部使用
匿名空间中的实体只能在本文件的作用域内有效,它的作用域是从匿名命名空间声明开始到本文件结束。
extern与include的对比
- extern外部引入的方式适合管理较小的代码组织,用什么就引入什么,但是如果跨模块调用的关系不清晰,容易出错;
- include头文件的方式在代码组织上更清晰,但是会一次引入全部内容,相较而言效率比较低。
使用命名空间的规则
命名空间可以多次定义,可以对命名空间进行扩展。
在同一个源文件中可以多次定义同名的命名空间,被认为是同一个命名空间,所以不能在其中定义相同的实体。
namespace wd {int num = 100;void print(){cout << "print()" << endl;} }//end of namespace wdnamespace wd {int num2 = 300;int num = 100;//error }//end of namespace wdvoid test0(){cout << wd::num << endl;cout << wd::num2 << endl; }
小结
命名空间的作用:
避免命名冲突:命名空间提供了一种将全局作用域划分成更小的作用域的机制,用于避免不同的代码中可能发生的命名冲突问题;
组织代码:将相关的实体放到同一个命名空间;
版本控制:不同版本的代码放到不同的命名空间中;
- 声明主权。
总之,需要用到代码分隔的情况就可以考虑使用命名空间。
命名空间使用指导原则:
提倡在已命名的名称空间中定义变量,而不是直接定义外部全局变量或者静态全局变量。
如果开发了一个函数库或者类库,提倡将其放在一个命名空间中。
对于using 声明,首先将其作用域设置为局部而不是全局。
包含头文件的顺序可能会影响程序的行为,如果非要使用using编译指令,建议放在所有#include预编译指令后。
规范补充: include多个头文件,首先放自定义的头文件,再放C的头文件,再放C++的头文件,最后放第三方库的头文件。
const关键字
修饰内置类型
const修饰的变量称为const常量,之后不能修改其值。(本质还是变量,使用时也是当成变量使用,只是被赋予只读属性)
整型、浮点型数据都可以修饰,它们被称为const常量。const常量在定义时必须初始化。
const int number1 = 10; int const number2 = 20;
C语言中是使用宏定义的方式创建常量
#define NUMBER 1024
const常量和宏定义常量的区别
发生的时机不同:C语言中的宏定义发生时机在预处理时,做字符串的替换;
const常量是在编译时(const常量本质还是一个变量,只是用const关键字限定之后,赋予只读属性,使用时依然是以变量的形式去使用)
类型和安全检查不同:宏定义没有类型,不做任何类型检查;const常量有具体的类型,在编译期会执行类型检查。
在使用中,应尽量以const替换宏定义常量,可以减小犯错误的概率。
修饰指针类型
以int指针为例,用const修饰有三种形式:
const int * p int const * p1 int * const p2
指向常量的指针(pointer to const)
const int *pint const *p
const在*左边,即为指向常量的指针,不能通过指针改变其指向的值,但是可以改变这个指针的指向。
int number1 = 10; int number2 = 20;const int * p1 = &number1;//指向常量的指针 *p1 = 100;//error 通过p1指针无法修改其所指内容的值 p1 = &numbers;//ok 可以改变p1指针的指向
例子中p1称为指向常量的指针(pointer to const),尽管number1本身并不是一个int常量,但定义指针p1的方式决定了无法通过p1修改其指向的值。但值得注意的是,修改p1的指向是允许的。
补充:如果有一个const常量,那么普通的指针也无法指向这个常量,只有指向常量的指针才可以。
const int x = 20; int * p = &x; //error const int * cp = &x; //ok
常量指针(const pointer)
int * const p
const在*右边,即为常量指针,不能改变这个指针的指向,但是可以通过指针改变其指向的值。
int * const p3 = &number1;//常量指针 *p3 = 100;//ok 通过p3指针可以修改其所指内容的值 p3 = &number2;//error 不可以改变p1指针的指向
双重const限定的指针
- 既不能修改指向
- 又不能修改指向内容的数据
数组指针/指针数组
数组指针
指向数组的指针 pointer to array , 本质是指针, 指向数组首地址的指针
int (*p)[3];
指针数组
元素都是指针类型的数组 array of pointers , 本质是数组,其元素是指针
int *p[3];
函数指针/指针函数
函数指针
指向函数的指针 pointer to function ,本质是指针
可以通过函数指针调用函数
return_type (*pointer_name)(parameter_list);
指针函数
返回值为指针类型的函数 function return a pointer , 本质为函数
return_type* function_name(parameter_list){};
new/delete运算符
C/C++申请、释放堆空间的方式
C语言中使用malloc/free函数进行动态内存分配
使用malloc分配内存
void * malloc(size_t size)
初始化使用完毕后进行free释放空间
为避免空指针,将指针置为NULL
处理基本数据类型的数据
// 注意malloc的返回值为void * , 需要强制转换为相应指针类型 int * p = (int*)malloc(sizeof(int)); *p = 10; free(p); p = NULL;
处理数组类型的数据
// C中动态分配数组空间 int size = 3; int *p = (int *)malloc(size * sizeof(int)); for(int i = 0; i < size; i++){p[i] = i; }
C++使用new/delete运算符
使用new表达式分配空间,并初始化
使用delete表达式释放空间
将指针置为NULL,使用nullptr
new语句中可以不加参数,初始化为各类型默认值;也可加参数,参数代表要初始化的值
处理基本类型数据
// 第一种写法 //初始化为该类型的默认值 int * p = new int{}; cout << *p << endl; // 值初始化 int * p2 = new int{1}; cout << *p2 << endl;// 第二种写法 //初始化为该类型的默认值 int * p3= new int(); cout << *p3 << endl; // 值初始化 int * p4 = new int(1); cout << *p4 << endl;// 释放空间delete表达式 delete p1; delete p2; delete p3; delete p4; // 将指针置为NULL p1 = nullptr; p2 = nullptr; p3 = nullptr; p4 = nullptr;
处理数组类型的数据
// 默认初始化元素为int类型默认值 0 int * p3 = new int[10]{}; for(int idx = 0; idx < 10; ++idx){cout << p3[idx] << endl; } // 对于数组数据要使用delete[] delete [] p3; p3 = nullptr;// 使用值初始化 int * p4 = new int[3]{1,2,3}; for(int idx = 0; idx < 3; ++idx){cout << p4[idx] << endl; } // delete [] 释放 delete [] p4; p4 = nullptr;
malloc/free 和 new/delete 的区别
malloc/free是库函数;new/delete是运算符,后两者使用时不是函数的写法;
new表达式的返回值是相应类型的指针,malloc返回值是void*;
malloc申请的空间不会进行初始化,获取到的空间是有脏数据的,但new表达式申请空间时可以直接初始化;
malloc的参数是字节数,new表达式不需要传递字节数,会根据相应类型自动获取空间大小。
valgrind工具集
valgrind是一种开源工具集,它提供了一系列用于调试和分析程序的工具。其中最为常用和强大的工具就是memcheck。它是valgrind中的一个内存错误检查器,它能够对C/C++程序进行内存泄漏检测、非法内存访问检测等工作。
安装命令:
sudo apt install valgrind
安装完成后即可通过memcheck工具查看内存泄漏情况,编译后输入如下指令
valgrind --tool=memcheck ./a.out
如果想要更详细的泄漏情况,如造成泄漏的代码定位,编译时加上-g
valgrind --tool=memcheck --leak-check=full ./a.out
但是这么长的指令使用起来不方便,每查一次就得输入一次,可以设置一下。
- 在home目录下编辑.bashrc文件,改别名
alias memcheck='valgrind --tool=memcheck --leak-check=full --show-reachable=yes'
重新加载
source .bashrc
改写之后,就可以直接使用memcheck指令查看内存泄漏情况
memcheck ./a.out
- definitely lost: 绝对泄漏了;
- indirectly lost: 间接泄漏了;
- possibly lost: 可能泄漏了,基本不会出现;
- still reachable: 没有被回收,但是不确定要不要回收;
- suppressed :被编译器自动回收了,不用管
如上发生了两处泄漏,一共泄漏了8个字节,此时需要对new表达式申请的空间进行回收
回收空间时的注意事项
三组申请空间和回收空间的匹配组合
- malloc free
- new delete
- new int[5]() delete[]
如果没有匹配,memcheck会报出错误匹配的信息,实际开发中有可能回收掉了预期外的信息。
安全回收
delete只是回收了指针指向的空间,但这个指针变量依然还在,指向了已经被回收的内存区域(悬挂指针),继续使用会导致未定义行为,容易造成错误。所以需要进行安全回收,将这个指针设为空指针。C++11之后使用nullptr表示空指针。
int * p1 = new int();//初始化为该类型的默认值cout << *p1 << endl;delete p1;p1 = nullptr;//安全回收
引用
引用的概念
在C++中,在逻辑层面上(在使用时),引用是一个已定义变量的别名。
其语法是:
//定义方式:类型 & ref = 变量; int number = 2; int & ref = number;
在使用引用的过程中,要注意以下几点:
&在这里不再是取地址符号,而是引用符号
引用的类型需要和其绑定的变量的类型相同(目前这样使用,学习继承后这一条有所不同)
声明引用的同时,必须对引用进行初始化,否则编译时报错
引用一经绑定,无法更改绑定
void test0(){int num = 100;int & ref = num;//声明ref时进行了初始化(绑定)//int & ref2; //error 没有进行初始化cout << num << endl;cout << ref << endl;cout << &num << endl;cout << &ref << endl;int num2 = 200;ref = num2;//这里只是一个赋值操作,并不是更改绑定 }
引用的本质
C++中的引用本质上是一种被限制的指针。类似于线性表和栈的关系,栈是被限制的线性表,底层实现相同,只不过逻辑上的用法不同而已。
由于引用是被限制的指针,所以引用是占据内存的,占据的大小就是一个指针的大小。有很多的说法,都说引用不会占据存储空间,其只是一个变量的别名,但这种说法并不准确。引用变量会占据存储空间,存放的是一个地址,但是编译器阻止对它本身的任何访问,从一而终总是指向初始的目标单元。在汇编里,引用的本质就是“间接寻址”。
可以尝试对引用取址,发现获取到的地址就是引用所绑定变量的地址。
void test(){int num = 100;int *p = #cout << &p << endl;cout << p << endl;cout << #int & ref = num;// 定义引用ref 绑定num变量// 引用的底层也是指针实现的(常量指针 const pointer) 但是无法访问创建的指针变量// 对引用取地址获取到的其实是其绑定的变量的地址cout << &ref << endl;cout << ref << endl; }
引用与指针的联系与区别
联系
引用和指针都有地址的概念,都是用来间接访问变量;
引用的底层还是指针来完成,可以把引用视为一个受限制的指针。(const pointer)
区别
引用必须初始化,指针可以不初始化;
引用不能修改绑定,但是指针可以修改指向;
在代码层面对引用本身取址取到的是变量本体的地址,但是对指针取址取到的是指针变量的地址
引用的使用场景
引用作为函数的参数
引用作为函数的返回值
引用作为函数的参数
在没有引用之前,如果我们想通过形参改变实参的值,只有使用指针才能到达目的。但使用指针的过程中,不好操作,很容易犯错。而引用既然可以作为其他变量的别人而存在,那在很多场合下就可以用引用代替指针,因而也具有更好的可读性和实用性。这就是引用存在的意义。
一个经典的例子就是交换两个变量的值,请实现一个函数,能够交换两个int型变量的值:
void swap(int x, int y){//值传递,发生复制int temp = x;x = y;y = temp; }void swap2(int * px, int * py){//地址传递,不复制int temp = *px;*px = *py;*py = temp; }//在实参传给swap3时, //其实就是发生了初始化int & x = a; //int & y = b; void swap3(int & x, int & y){//引用传递,不复制int temp = x;x = y;y = temp; }
补充:之后,如果一个函数的功能不需要改变实参本身的值,而且参数类型是内置类型,可以依然使用值传递;
引用传递作为函数参数,会初始化引用,因为引用的底层是指针实现,所以也会有额外开销(比较小),如果函数参数是较大的对象或数据,那么使用引用作为函数参数可以避免复制实参,这样做可以减少开销。
当然,如果函数中需要改变实参本身的内容,值传递就无法实现了,需要引用传递(或者地址传递)。
参数传递的方式
值传递
采用值传递时,系统会在内存中开辟空间用来存储形参变量,并将实参变量的值拷贝给形参变量。
也就是说形参变量只是实参变量的副本而已;如果函数传递的是类对象,而该对象占据的存储空间比较大,那发生复制就会造成较大的不必要开销。
这种情况下,强烈建议使用引用作为函数的形参,这样会大大提高函数的时空效率。
指针传递
使用指针作为函数的形参虽然达到的效果和使用引用一样,但当调用函数时仍需要为形参指针变量在内存中分配空间,也由于指针的灵活更可能导致问题的产生,故在C++中推荐使用引用而非指针作为函数的参数。
引用传递
当用引用作为函数的参数时,其效果和用指针作为函数参数的效果相当。当调用函数时,函数中的形参就会被当成实参变量或对象的一个别名来使用,也就是说此时函数中对形参的各种操作实际上是对实参本身进行操作,而非简单的将实参变量或对象的值拷贝给形参。
常引用:使用const修饰的引用
如果不希望函数体中通过引用改变传入的变量,那么可以使用常引用作为函数参数
不会修改值
不会复制(不会造成不必要的开销)
// 常引用基本特点 void test1(){int num = 10;// 定义引用绑定num 并使用const修饰const int & ref = 10;// 既不能修改指向,也不能通过这个引用修改变量的值// ref = 100; // error read onlynum = 100;cout << "num = " << num << endl;cout << "ref = " << ref << endl;// 不能通过引用常引用修改 但是可以通过变量自身修改 }// 函数不希望通过引用改变变量的值的时候可以使用常引用 // 形参为常引用 void func(const int & x){cout << x << endl;// x = 100; //error read only 无法通过常引用修改 }void test(){int num = 1;func(num);cout << num << endl; }
引用作为函数的返回值
以引用作为函数的返回值时,返回的变量其生命周期一定是要大于函数的生命周期的,即当函数执行完毕时,返回的变量还存在。避免复制,节省开销
int a = 1; // 返回值为int类型 int func(){//...return a; //在函数内部,当执行return语句时,会发生复制 } // 返回值为int类型的引用 int & func2(){//...return b; //在函数内部,当执行return语句时,不会发生复制 }
// 全局变量 int a = 100; int func(){// func函数返回的是a的一个副本,一个临时变量return a; }// 全局变量 int b = 200; // 函数返回值为引用 int & func2(){// return时不会发生复制return b; // 返回的实际是一个绑定到b的引用 // 要注意返回的引用所绑定的变量的生命周期要比函数更长 }void test(){cout << func() << endl;cout << &a << endl;// cout << &func() << endl;// error // func()返回的是一个临时变量值,不允许对一个临时变量取地址,一个临时值没有地址返回给调用者cout << func2() << endl;cout << &func2() << endl; // OK func2 返回的是引用不是值. }
注意事项
不要返回局部变量的引用
因为局部变量会在函数返回后被销毁,被返回的引用就成为了"无所指"的引用,程序会进入未知状态。
int & func() {int number = 1;return number; }
不要轻易返回一个堆空间变量的引用
容易造成内存泄漏
int & func() {int * p_int = new int(1);return *p_int; }void test() {int a = 2, b = 4;int c = a + func() + b;//内存泄漏 }
如果函数返回的是一个堆空间变量的引用,那么这个函数调用一次就会new一次,非常容易造成内存泄露。所以谨慎使用这种写法,并且要有完善的回收机制。
int & func3(){int *p = new int{10};return *p; }void test(){// func3调用1次就会new一次, 如果不释放就会内存泄漏//cout << func3() << endl;//delete &func3();// 调用2次func3,释放一次,仍然泄露// 完善写法,使用引用接收之后再处理int &ref = func3();cout << ref << endl;// deletedelete &ref; }
总结
在引用的使用中,单纯给某个变量取个别名没有什么意义,引用的目的主要用于在函数参数传递中,解决大块数据或对象的传递效率和空间不理想的问题。
用引用传递函数的参数,能保证参数传递中不产生副本,提高传递的效率,还可以通过const的使用,保证了引用传递的安全性。
引用与指针的区别是,指针通过某个指针变量指向一个变量后,对它所指向的变量间接操作。程序中使用指针,程序的可读性差;引用底层仍然是指针,但是编译器不允许访问到这个底层的指针,逻辑上简单理解为——对引用的操作就是对目标变量的操作。可以用指针或引用解决的问题,更推荐使用引用。
强制转换
C语言中的强制转换在C++代码中依然可以使用,这种C风格的转换格式非常简单
TYPE a = (TYPE)EXPRESSION;
特点
简单直接:只需指定目标类型即可完成类型转换。
无类型安全:C风格的类型转换直接将数据类型转换为指定类型,但缺乏安全检查,容易导致潜在的错误。
灵活但容易出错:由于转换是强制性的,可能会发生不符合预期的类型转换,尤其是指针类型转换时,很容易出现错误 , 导致未定义行为
c风格的转换不容易查找:它由一个括号加上一个标识符组成,而这样的东西在c++程序里一大堆。
void func(){double d = 3.14;// double指针强转为int指针 编译虽然通过 但是会导致未定义行为int *p = (int *)&d;// 未定义行为cout << *p << endl; // 输出结果无法预测 }
c++为了克服这些缺点,引进了4个新的类型转换操作符:
static_cast
const_cast
dynamic_cast
reinterpret_cast
static_cast
最常用的类型转换符,在正常状况下的类型转换, 用于将一种数据类型转换成另一种数据类型,如把int转换为float
基本语法:
目标类型 转换后的变量 = static_cast<目标类型>(要转换的变量)
static_cast的用法主要有以下几种:
(1) 用于基本数据类型之间的转换
int iNumber = 100; float fNumber = 0; fNumber = static_cast<float>(iNumber);
(2) 把void指针转换成目标类型的指针,但不安全(可能因为指向的实际数据类型和期望类型不一致导致未定义行为,编译器不会检查)
void * pVoid = malloc(sizeof(int)); // void * ---> int * int * pInt = static_cast<int*>(pVoid); *pInt = 1;
(3) 用于类层次结构中基类和子类之间指针或引用的转换
注意: 不能完成任意两个指针类型间的转换
int iNumber = 1; int * pInt = &iNumber; float * pFloat = static_cast<float *>(pInt);//error
好处: 查找方便 grep -rn "static_cast" ./ 查找那个具体文件中使用了强制转换,要比C中的强转方便一些.
const_cast
该运算符用来修改指针/引用的const属性,基本不用,仅做了解。
使用形式:
1.指向常量的指针被转化成普通指针,并且仍然指向原来的对象;
const int * p 指向常量
const int * p 指向普通变量
// 定义常量 const int number = 100; //int * p_int = &number;//error read only // 去除const属性 int * p_int2 = const_cast<int *>(&number); // OK 转换为普通指针 // 以下代码都是未定义行为,不合法 // 通过指针修改值 *p_int2 = 10; //后续属于未定义行为 (C++标准中,修改一个原本声明为const的变量属于未定义行为) cout << number << endl; cout << *p_int2 << endl;// 值居然不一样 cout << &number << endl; cout << p_int2 << endl;// 地址一样// 定义普通变量 int num = 1; // 指向常量的指针指向num 无法通过该指针修改变量数据 const int *p1 = # // 去除const属性 int *p2 = const_cast<int*>(p1); *p2 = 2; // OK cout << num << endl; cout << *p2 << endl;
2.常量引用被转换成非常量引用,并且仍然指向原来的对象;
// 常量引用转为非常量引用 int num = 100; const int & ref = num; // ref = 101;// error read only int & ref2 = const_cast<int&>(ref); ref2 = 101; cout << "num = " << num << endl; cout << "ref = "<< ref<< endl;
dynamic_cast
该运算符主要用于基类和派生类间的转换,尤其是向下转型的用法中
reinterpret_cast
万能转换
该运算符可以用来处理无关类型之间的转换,即用在任意指针(或引用)类型之间的转换,以及指针与足够大的整数类型之间的转换。由此可以看出,reinterpret_cast的效果很强大,但错误的使用reinterpret_cast很容易导致程序的不安全,只有将转换后的类型值转换回到其原始类型,这样才是正确使用reinterpret_cast方式。
函数重载
C++ 允许多个函数拥有相同的名字,只要它们的参数列表不同就可以,这就是函数重载(Function Overloading)。借助重载,一个函数名可以有多种用途。
在同一作用域内,可以有一组具有相同函数名,不同参数列表的函数,这组函数被称为重载函数。
注意:C 语言中不支持函数重载,C++才支持函数重载。
// 函数名相同 参数列表不同 int add(int x, int y) {return x + y; }int add(int x, int y, int z) {return x + y + z; }int add(float x, int y) {return x + y; }int add(int x, float y) {return x + y; }
实现函数重载的条件
函数名相同, 参数列表不同可以构成重载。
函数参数的数量
数量相同,类型不同
数量,类型都相同,参数的顺序不同
注意: 只有返回类型不同,参数完全相同,是不能构成重载的
int add(int x, int y) {return x + y; } // 只有返回值类型不同, 不能构成重载 void add(int x, int y) {cout << x + y << endl; }// 不是重载
函数重载的实现原理
实现原理: 名字改编(name mangling)——当函数名称相同时 ,会根据参数的类型、顺序、个数进行改编
g++ -c Overload.cc
nm Overload.o
查看目标文件,可以发现原本的函数名都被改编成与参数相关的函数名。
而C语言没有名字改编机制。
分析:C++的函数重载提供了一个便利,以前C语言要想实现各种不同类型参数的计算需要定义多个不同名字的函数,在调用函数时要注意参数的信息和函数名匹配。
C++有了函数重载,想要对不同类型的参数进行计算时,就可以使用同一个函数名字(代码层面的同名,编译器会处理成不同的函数名)。
缺点在于,C++编译器进行编译时比C的编译器多了一个步骤,效率有所降低。
extern "C"
在C/C++混合编程的场景下,如果在C++代码中想要对部分内容按照C的方式编译,应该怎么办?
extern "C" void func() //用 extern"C"修饰单个函数 {}//如果是多个函数都希望用C的方式编译 //或是需要使用C语言的库文件 //都可以放到如下{}中 extern "C" { //…… }
函数默认参数
在C++中,函数默认参数是指在定义函数时为一个或多个参数提供的默认值,当调用函数时如果没有为这些参数传递具体的值,则使用默认值。
函数默认参数的目的
函数调用时进行缺省调用
减少函数重载
函数提供默认参数可以进行缺省调用
C++可以给函数定义默认参数值。通常,调用函数时,要为函数的每个参数给定对应的实参。
void func(int x, int y) {cout << "x = " << x << endl;cout << "y = " << y << endl; } // 无论何时调用func1函数,都必须要给其传递两个参数。
但C++可以给参数定义默认值,如果将func1函数参数中的x定义成默认值0, y定义成默认值0
void func(int x = 0, int y = 0){cout << "x = " << x << endl;cout << "y = " << y << endl; }void test0(){// 如果传参,实际参数就是传递的参数, 如果不传,那么参数就是默认参数func(24,30);func(100);func(); }
这样调用时,若不给参数传递实参,则func1函数会按指定的默认值进行工作,即缺省调用。
给函数参数赋默认值后就可以进行缺省调用,但是传入的参数优先级高于默认参数。
减少函数重载
默认参数可将一系列简单的重载函数合成为一个。
void func3(); void func3(int x); void func3(int x, int y); //上面3个函数其实是函数重载 //上面三个函数可以合成下面这一个带默认参数的函数 void func3(int x = 0, int y = 0);
通过使用带默认参数的函数来减少函数重载的数量
注意:
如果一组重载函数(可能带有默认参数)都允许相同实参个数的调用,将会引起调用的二义性。
void func4(int x); void func4(int x, int y = 0);func4(1);//error,编译器无法确定调用的是哪种形式的func4
所以在函数重载时,要谨慎使用默认参数。重载是允许的,但是缺省调用时会产生冲突。应避免在同一个范围内定义带有默认参数的重载函数,否则编译器可能会因为无法确定合适的调用而报错。
默认参数的声明
一般默认参数在函数声明中提供。
当一个函数既有声明又有定义时,只需要在其中一个中设置默认值即可。
若在定义时而不是在声明时设置默认值,那么函数定义一定要在函数的调用之前 , 因为声明时已经给编译器一个该函数的向导,在定义时设默认值时,编译器只有检查到定义时才知道函数使用了默认值。若先调用后定义,在调用时编译器并不知道哪个参数设了默认值。
//这样可以编译通过 // 函数声明 void func(int x,int y);void test0(){// 函数调用 func(1,2); } // 函数定义 void func(int x,int y){cout << x + y << endl; }
//这样无法缺省调用 // 函数声明 void func(int x,int y);void test0(){func();//error 函数定义在函数调用之后 } // 函数定义 void func(int x = 0,int y = 0){cout << x + y << endl; }
所以我们通常是将默认值的设置放在声明中而不是定义中。
注意:
如果在声明中和定义中都传了默认值,会报错
// 函数定义 void func(int x = 0, int y = 0); // 函数声明 void func(int x = 0, int y = 0){ // error 重复设置默认值cout << "x = " << x << endl;cout << "y = " << y << endl; }
默认参数的顺序规定
如果一个函数中有多个默认参数,则形参分布中,默认参数应从右至左逐渐定义。
当调用函数时,只能从左向右匹配参数。如:
void func2(int a = 1, int b, int c = 0, int d);//error void func2(int a, int b, int c = 0, int d = 0);//ok
若给某一参数设置了默认值,那么在参数表中其后所有的参数都必须也设置默认值,否则,由于函数调用时可不列出已设置默认值的参数,编译器无法判断在调用时是否有参数遗漏。
完成函数默认参数的设置后,该函数就可以按照相应的缺省形式进行调用。
总结:函数参数赋默认值从右向左(严格),保证在缺省调用时完成准确的匹配
bool类型
bool类型是在C++中一种基本类型,用来表示true和false。true和false是字面值,可以通过转换变为int类型,true为1,false为0.
任何数字或指针值都可以隐式转换为bool值。
任何非零值都将转换为true,而零值转换为false(注意:-1也是代表true)
bool变量占1个字节的空间。
inline函数
在C语言中,我们使用带参数的宏定义这种借助编译器的优化技术来减少程序的执行时间,编译预处理器用文本替换的方式取代函数调用,省去了参数压栈、生成汇编语言的CALL调用、返回参数、执行return等过程,从而提高了速度。宏代码本身不是函数,但是看起来像函数。使用宏代码最大的缺点是容易出错,预处理器在文本替换时常常产生意向不到的边际效应。
例如:
#define MAX(a, b) (a) > (b) ? (a) : (b) int result = MAX(20,10) + 20//result的值是多少?int result2 = MAX(10,20) + 20//result2的值是多少?//result = MAX(i, j) + 20; 将被预处理器扩展为: result = (i) > (j) ?(i):(j)+20
修改宏代码
#define MAX(a, b) ((a) > (b) ? (a) : (b))
可以解决上面的错误了,但也不是万无一失的.
例如:
int i = 4,j = 3; int result = MAX(i++,j); cout << result << endl; //result = 5; cout << i << endl; //i = 6; //使用MAX的代码段经过预处理器扩展后,result = ((i++) > (j) ? (i++):(j));
宏的另一个缺点就是不可调试
那么在C++中有没有类似于宏的优化手段,但没有宏的缺点的处理方式呢?
答案是有的,那就是内联(inline)函数。内联函数作为编译器优化手段的一种技术,在降低运行时间上非常有用。
什么是内联函数
内联函数是C++的增强特性之一,用来降低程序的运行时间。
在代码中在一个函数的定义之前加上
inline
关键字,就是对编译器提出了内联的建议。如果建议通过,就会进行内联展开。
当内联函数收到编译器的指示时,即可发生内联:编译器将使用函数的定义体来替代函数调用语句,这种替代行为发生在编译阶段而非程序运行阶段。
定义函数时,在函数的最前面以关键字“inline”声明函数,该函数即可称为内联函数(内联声明函数)。
// 内联函数 inline int max(int x, int y) {return x > y ? x : y; }void test() {cout << max(1,2) << endl;// 在编译阶段 编译器采用了内联建议 内联展开:使用函数定义替代函数调用 cout << 1 > 2 ? 1:2 << endl// 使用内联函数 不会有函数调用的开销 }
使用inline函数来验证之前宏处理时遇到的问题
// 内联函数 inline void max(int x, int y) {return x > y ? x : y; }void test() {int i = 4,j = 3;int result = max(i++,j);cout << result << endl; //result = 4;cout << i << endl; //i = 5; }
内联函数原理
对于任何内联函数,编译器在符号表(符号表是编译器用来收集和保存字面常量和某些符号常量的地方)里放入函数的声明,包括名字、参数类型、返回值类型。
如果编译器没有发现内联函数存在错误,那么该函数的代码也会被放入符号表里。
在调用一个内联函数时,编译器首先检查调用是否正确(进行类型安全检查,或者进行自动类型转换)。
如果正确,内联函数的代码就会直接替换函数调用语句,于是省去了函数调用的开销。
这个过程与预处理有显著的不同,因为预处理器不能执行类型安全检查和自动类型转换。
—— 内联函数就是在普通函数定义之前加上inline关键字。
- inline是一个建议,并不是强制性的,可能会失效
- inline的建议如果有效,就会在编译时展开,可以理解为是一种更高级的代码替换机制(类似于宏——预处理)
- 函数体内容如果太长或者有循环之类的结构,不建议inline,以免造成代码膨胀;比较短小并且比较常用的代码适合用inline。
比如函数体中有循环结构,那么执行函数体的开销比调用函数的开销大得多,设为内联函数只能减少函数调用的开销,没有太大意义。
C++的函数内联机制既具备宏代码的效率,又增加了安全性,而且可以自由操作类的数据成员,所以在C++中应尽可能的用内联函数取代宏函数。
宏 VS 内联函数
适用场景
内联函数适用于需要提高性能的小型、频繁调用的函数,特别是需要进行类型检查和避免副作用的场景。对于需要安全性和封装性的代码段,应优先使用内联函数。
宏适用于简单的文本替换、条件编译、或者需要通用计算而不考虑类型的情况下。然而,应该尽量避免使用宏函数来实现复杂的逻辑.
总体而言,内联函数比宏更安全、更易读、更易于调试。在现代C++开发中,宏更多用于常量定义和条件编译,而逻辑操作和计算应尽量使用内联函数以确保代码的健壮性和可维护性。
内联函数注意事项
1.如果内联函数采用声明和实现分离的写法
// 声明 inline int add(int x ,int y);// 实现 int add(int x ,int y){return x + y; }void test() {// 按照普通函数的形式调用cout << add(1,2) << endl; }
调用一个函数时,是采取内联函数的方式还是普通函数的方式,取决于该函数的实现—— 下面两种写法都会按照内联函数的方式展开。
inline int add1(int x ,int y);inline int add1(int x ,int y){return x + y; }int add2(int x ,int y);inline int add2(int x ,int y){return x + y; }void test() {cout << add1(1,2) << endl;cout << add2(1,2) << endl; }
建议: 在声明和定义处都加上inline, 使得函数更加明确.
如果要把inline函数声明在头文件中,则必须把函数定义也写在头文件中。若头文件中只有声明没有实现,被认为是没有定义替换规则。
如下,foo函数不能成为内联函数:
inline void foo(int x, int y);//该语句在头文件中void foo(int x, int y)//实现在.cpp文件中 { //... }
因为编译器在调用点内联展开函数的代码时,必须能够找到 inline函数的定义才能将调用函数替换为函数代码,而对于在头文件中仅有函数声明是不够的。
当然内联函数定义也可以放在源文件中,但此时只有定义的那个源文件可以用它,而且需要为每个源文件拷贝一份内联函数的定义(每个源文件里的定义必须是完全相同的)。相比之下,放在头文件中既能够确保调用函数的定义是相同的,又能够保证在调用点能够找到函数定义从而完成内联(替换)。从测试文件出发,找到头文件,发现此函数是inline函数,那么要展开替换,必须要有明确的替换规则,但是在头文件中并没有发现替换规则,则报错未定义问题。 inline函数在头文件必须有定义。
2.谨慎使用内联
内联能提高函数的执行效率,为什么不把所有的函数都定义成内联函数?事实上,内联不是万灵丹,它以代码膨胀(拷贝)为代价,仅仅省去了函数调用的开销,从而提高程序的执行效率。(注意:这里的“函数调用开销”是指参数压栈、跳转、退栈和返回等操作)
如果执行函数体内代码的时间比函数调用的开销大得多,那么 inline 的效率收益会很小。另外,每一处内联函数的调用都要拷贝代码,将使程序的总代码量增大,消耗更多的内存空间。以下情况不宜使用内联:
如果函数体内的代码比较长,使用内联将导致可执行代码膨胀过大。
如果函数体内出现循环或其他复杂的控制结构,那么执行函数体内代码的时间将比函数调用开销大得多,因此内联的意义并不大。
实际上,inline 在实现的时候就是对编译器的一种请求,因此编译器完全有权利取消一个函数的内联请求。一个好的编译器能够根据函数的定义体,自动取消不值得的内联,或自动地内联一些没有inline 请求的函数。因此编译器往往选择那些短小而简单的函数来内联。
异常处理
异常是描述程序在执行期间产生的问题。
异常处理是用于处理运行时错误的机制。它允许开发者在程序中检测错误并采取相应的补救措施,从而使程序能够优雅地处理错误并继续运行C++ 异常是指在程序运行时发生的特殊情况,比如尝试除以零的操作。异常提供了一种转移程序控制权的方式。C++ 异常处理涉及到三个关键字:try、catch、throw.
抛出异常即检测是否产生异常,在 C++ 中,其采用 throw 语句来实现,如果检测到产生异常,则抛出异常。该语句的格式为:
throw 表达式;
先定义抛出异常的规则(throw),异常是一个表达式,它的值可以是基本类型,也可以是类类型;
double division(double x, double y) {if(y == 0)throw "Division by zero condition!";return x / y; }
try-catch语句块的语法如下:
try { //语句块 } catch(异常类型) { //具体的异常处理... } ... catch(异常类型) { //具体的异常处理... }
try-catch语句块的catch可以有多个,至少要有一个,否则会报错。
执行 try 块中的语句,如果执行的过程中没有异常拋出,那么执行完后就执行最后一个 catch块后面的语句,所有 catch 块中的语句都不会被执行;
如果 try 块执行的过程中拋出了异常,那么拋出异常后立即跳转到第一个“异常类型”和拋出的异常类型匹配的 catch 块中执行(称作异常被该 catch 块“捕获”),执行完后再跳转到最后一个catch 块后面继续执行。
注意:catch的是类型,不是具体信息。
double division(double x,double y){if(y == 0){throw "Deivision by zero";}return x/y; }void test0(){double x = 100, y = 0;try{cout << "before" << endl;cout << division(x,y) << endl;//异常行后的代码不会执行cout << "after" << endl;}catch(const char * msg){ //catch的小括号里是类型cout << "hello" << endl;cout << "hello," << msg << endl;}catch(double x){cout << "double" << endl;}catch(int x){cout << "int" << endl;}cout << "end test" << endl; }
内存布局
64位系统,理论空间达到16EB(2^64),但是受硬件限制,并不会达到这么多;
以32位系统为例,一个进程在执行时,能够访问的空间是虚拟地址空间。理论上为2^32,即4G,有1G左右的空间是内核态,剩下的3G左右的空间是用户态。从高地址到低地址可以分为五个区域:
栈区:操作系统控制,由高地址向低地址生长,编译器做了优化,显示地址时栈区和其他区域保持一致的方向。
堆区:程序员分配,由低地址向高地址生长,堆区与栈区没有明确的界限。
全局/静态区:读写段(数据段),存放全局变量、静态变量。
文字常量区:只读段,存放程序中直接使用的常量,如const char * p = "hello"; hello这个内容就存在文字常量区。
程序代码区:只读段,存放函数体的二进制代码。
注意:
因为编译器的优化, 局部变量的地址分配通常是:
后创建的变量分配高地址
先创建的变量分配低地址
C风格字符串
两种形式
字符数组形式,注意留出一位给终止符'\0';
字符指针形式,使用字符串常量时, 直接定义为const char * ,C++代码中标准C风格字符串的写法。
字符指针形式, 并在堆上申请空间
输出流运算符默认重载,cout利用输出流运算符接char型数组名、指针名时,输出的是内容,而不是地址。
字符数组形式
void test1(){// 字符数组形式char str1[] = "hello";// 等价于下面 最后一位'\0'char str2[] = {'h','e','l','l','o','\0'};// cout输出流运算符默认重载,连接char类型的数组名/指针名时// 输出的是内容,而不是地址cout << "str1 = " << str1 << endl; // hellocout << "str2 = " << str2 << endl; // hellostr1[0] = 'H';cout << "str1 = " << str1 << endl; // Hello}
字符指针形式
// 字符指针形式 //c++标准 要使用const char * // 即指向常量的指针,不能通过指针修改 const char *str1 = "hello"; const char *str2 = "world"; cout << "str1 = " << str1 << endl; cout << "str2 = " << str2 << endl; /* str1[0] = "H"; // error read only*/ str1 = "wd"; cout << "str1 = " << str1 << endl;
应用
- 获取字符串长度
- 字符串copy
- 字符串拼接
- ......
void test3(){// 字符串复制const char *str1 = "hello";cout << "str len:" << strlen(str1) << endl;char *str2 = (char *)malloc(strlen(str1)+1);strcpy(str2,str1);cout << "str2 len: " << strlen(str2 )<< endl;cout << "str2 = " << str2 << endl;free(str2);str2 = NULL; }void test4(){// C字符串拼接const char *str1 = "hello";const char *str2 = ",world";size_t len = strlen(str1) + strlen(str2) + 1;char *str = new char[len]{};strcat(str,str1);strcat(str,str2);cout << "str = " << str << endl;delete [] str;str = nullptr; }