C++与C
文章目录
- C++与C
- 命令空间
- const关键字
- new/delete表达式
- 引用(重点)
- 概念
- 引用的本质
- 引用的使用场景
- 引用作为函数的参数
- 引用作为函数的返回值
- 总结
- 强制转换
- 函数重载
- extern "C"
- 默认参数
- bool类型
- inline(内联)函数
- 异常处理(了解)
- 内存布局(重要)
- C风格字符串
- 练习
C++与C
命令空间
为什么要使用命名空间——为了避免名字冲突
什么是命名空间——名字空间,在命名空间中的实体与全局实体隔离,存放在名字空间的实体只在本空间有效,类似于文件夹。
命名空间的使用方式:
using编译指令——全部引入,容易引发命名空间的污染
#include <iostream> using namespace std; //using编译指令int main(int argc, char * argv[]){cout << "hello,world" << endl;return 0; }
作用域限定符——比较麻烦
namespace wd { int number = 10; void display() {//cout,endl都是std空间中的实体,所以都加上'std::'命名空间std::cout << "wd::display()" << std::endl; } }//end of namespace wdvoid test0() {std::cout << "wd::number = " << wd::number << endl;wd::display(); }
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; }
命名空间的嵌套使用
匿名命名空间——希望一部分实体只在本文件中起作用,那么可以将它们定义在匿名空间中。
跨模块调用问题
全局变量和函数是可以跨模块调用的
有名命名空间的实体可以跨模块调用
// 通过extern引入实体//externA.cc namespace wd { int val = 300; void display(){ cout << "wd::display()" << endl; } }//end of namespace wd// //externB.cc namespace wd { extern int val; extern void display(); }void test0(){ cout << wd::val << endl; wd::display(); }
extern与include的对比:
extern外部引入的方式适合管理较小的代码组织,用什么就引入什么,但是如果跨模块调用的关系不清晰,容易出错;
include头文件的方式在代码组织上更清晰,但是会一次引入全部内容,相较而言效率比较低。
- 使用命名空间的规则
命名空间中的实体一定在命名空间之外使用,可以理解为命名空间只是用来存放实体。
const关键字
const修饰的变量称为const常量,之后不能修改其值。(本质还是变量,使用时也是当成变量使用,只是被赋予只读属性)
const常量在定义时必须初始化。
const int number1 = 10; int const number2 = 20;const int val;//error 常量必须要进行初始化
const常量和宏定义常量的区别
- 产生的阶段不同,宏定义发生在预处理阶段;const常量发生在编译阶段
- 宏定义只是进行简单的文本替换,并没有类型,不进行类型检查,不安全;const常量有类型,会进行类型检查
const修饰指针类型
const int* p; // 指向常量的指针 值不能改,指向可改 int * const p; // 常量指针 值可以改 指向不能改 const int* const p // 双重限定
数组指针与指针数组
- 数组指针:指向数组的指针
- 指针数组:数组中元素是指针
函数指针与指针函数
- 函数指针:指向函数的指针
- 指针函数:返回值是指针的函数
new/delete表达式
C语言中使用malloc/free函数,C++使用new/delete表达式
// new语句中可以不加参数,初始化为各类型默认值;也可加参数,参数代表要初始化的值int * p = (int*)malloc(sizeof(int)); *p = 10; free(p);int * p1 = new int(); //初始化为该类型的默认值 cout << *p1 << endl;int * p2 = new int(1); // 加参数,代表要初始化的值 cout << *p2 << endl;
- valgrind 工具集——用于调试和分析的工具,其中最为常用的工具是memcheck,用于检查内存错误如内存泄露、非法内存访问等。
malloc/free 和 new/delete 的区别
malloc
/free
是库函数;new
/delete
是表达式,后两者使用时不是函数的写法;new
表达式返回值是相应类型的指针,malloc
返回void*malloc
需要输入字节数,new表达式不需要传递字节数,会根据相应类型自动获取空间大小malloc
申请的空间不会初始化,会有脏数据;new表达式申请的空间可以直接初始化;int* p = new int(4); // 直接初始化 delete p;int* p = new int[10](); // 申请数组空间 delete [] p;int* p = new int[3]{1, 2, 3}; delete [] p;p = nullptr; // 安全回收
使用new语句申请数组空间需要使用delete [] p的形式回收堆空间
引用(重点)
概念
在C++中,在逻辑层面上(在使用时),引用是一个已定义变量的别名。
//定义方式: 类型 & ref = 变量; int num = 2; int& ref = num;
引用一经绑定,无法更改绑定
void test0(){int num = 100;int & ref = num;//声明ref时进行了初始化(绑定)//int & ref2; //error // 声明引用时,必须进行初始化cout << num << endl;cout << ref << endl;cout << &num << endl;cout << &ref << endl; }
引用的本质
C++中的引用本质上是一种被限制的指针。
引用的底层也是指针(常量指针)实现的,所以引用是占内存的,占据的大小就是一个指针的大小。
引用与指针的联系与区别
联系:
- 引用和指针都是用来间接访问变量
- 引用的底层还是指针,可视为一个被限制的指针
区别:
- 引用必须初始化,指针可以不初始化
- 引用不能修改绑定,但指针可以
- 在代码层面对引用本身取址取到的是变量本体的地址,但是对指针取址取到的是指针变量的地址。
引用的使用场景
引用作为函数的参数
当用引用作为函数的参数时,其效果和用指针作为函数参数的效果相当。当调用函数时,函数中的形参就会被当成实参变量或对象的一个别名来使用,也就是说此时函数中对形参的各种操作实际上是对实参本身进行操作,而非简单的将实参变量或对象的值拷贝给形参。
使用指针作为函数的形参虽然达到的效果和使用引用一样,但传参时要以地址形式传入(如果使用引用则可直接传入变量或对象),也由于指针的灵活更可能导致问题的产生,故在C++中推荐使用引用而非指针作为函数的参数。
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; }
引用作为函数的返回值
要求:当以引用作为函数的返回值时,返回的变量其生命周期一定是要大于函数的生命周期的,即当函数执行完毕时,返回的变量还存在。
目的: 避免复制,节省开销
int func(){//...return a; //在函数内部,当执行return语句时,会发生复制 } int & func2(){//...return b; //在函数内部,当执行return语句时,不会发生复制 }
注意事项:
- 不要返回局部变量的引用
- 不要轻易返回一个堆变量的引用,非常容易造成内存泄露
总结
- 在引用的使用中,单纯给某个变量取个别名没有什么意义,引用的目的主要用于在函数参数传递中,解决大块数据或对象的传递效率和空间不理想的问题。
- 用引用传递函数的参数,能保证参数传递中不产生副本,提高传递的效率,还可以通过const的使用,保证了引用传递的安全性。
- 引用与指针的区别
强制转换
C语言中的强制转换在C++代码中依然可以使用,这种C风格的转换格式非常简单
C风格强制转换的缺点
- 因为可以在任意类型之间转换,隐藏着巨大的风险
- C风格装换不容易查找
C++类型装换
static_cast:最常用的类型装换符
int iNumber = 100; float fNumber = 0; fNumber = (float) iNumber;//C风格 fNumber = static_cast<float>(iNumber);
const_cast:修改类型的const属性,基本不用
指向常量的指针被转化成普通指针,并且仍然指向原来的对象;
常量引用被转换成非常量引用,并且仍然指向原来的对象;
dynamic_cast:
reinterpret_cast:
函数重载
C 语言中不支持函数重载,C++才支持函数重载。
函数同名,但函数参数的数量、类型、顺序任一不同则可以构成重载
函数重载实现原理: 名字改编(name mangling)——当函数名称相同时 ,会根据参数的类型、顺序、个数进行改编
extern “C”
在C/C++混合编程的场景下,如果在C++代码中想要对部分内容按照C的方式编译,应该怎么办?
extern "C" void func() //用 extern"C"修饰单个函数 {}//如果是多个函数都希望用C的方式编译 //或是需要使用C语言的库文件 //都可以放到如下{}中 extern "C" { //…… }
默认参数
void func(int x = 0, int y = 0){cout << "x = " << x << endl;cout << "y = " << y << endl; }void test0(){func(24,30);func(100);func(); }
通常将默认值的设置放在声明中而不是定义中
注意:如果在声明中和定义中都传了默认值,会报错
函数参数赋默认值从右向左(严格)
bool类型
bool类型是在C++中一种基本类型,用来表示true和false。true和false是字面值,可以通过转换变为int类型,true为1,false为0.
任何非零值都将转换为true,而零值转换为false(注意:-1也是代表true)
bool变量占1个字节的空间。
bool b1 = -100; bool b2 = 100; bool b3 = 0; bool b4 = 1; bool b5 = true; bool b6 = false; int x = sizeof(bool);//x = 1
inline(内联)函数
如果一个函数的操作特别简单,那么调用函数的开销就比执行函数操作要大得多
简单的函数还可以用宏定义函数替换,虽然提升了效率,但没有安全检查的过程,容易出错,并且宏函数不可调试。
内联函数,作为编译器优化手段的一种技术,在降低运行时间上非常有用。
内联函数:使用
inline
函数不会有函数调用的开销。inline int max(int x, int y) {return x > y ? x : y; }
在代码中在一个函数的定义之前加上inline关键字,就是对编译器提出了内联的建议。如果建议通过,就会进行内联展开。
编译器将使用函数的定义体来替代函数调用语句,这种替代行为发生在编译阶段。
内联函数的代码直接替换函数调用的语句
简单操作的函数,推荐使用内联函数:
- inline是一个建议,并不是强制性的
- inline的建议如果有效,就会在编译时展开,可以理解为是一种更高级的代码替换机制
- 函数体内容如果太长或者有循环之类的结构,不建议inline,以免造成代码膨胀;比较短小并且比较常用的代码适合用inline。
对比总结
宏函数
优点:只是进行字符串的替换,并没有函数的开销,对于比较短小的代码适合使用;
缺点:没有类型检查,存在安全隐患,而且比较容易写错。
普通函数
优点:可调试,有类型检查,比宏函数更安全;
缺点:函数的调用会增加开销。
内联函数
既具备宏代码的效率,又增加了安全性,所以在C++中应尽可能的用内联函数取代宏函数。
异常处理(了解)
throw:抛出异常
double division(double x, double y) {if(y == 0)throw "Division by zero condition!";return x / y; }
try-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 << division(x,y) << endl;}catch(const char *){ //catch匹配的是异常的类型cout << "hello,there is an error" << endl;}catch(double x){cout << "double " << x << endl;} }
注意:catch的是类型,不是具体信息。
内存布局(重要)
以32位系统为例,一个进程在执行时,能够访问的空间是虚拟地址空间。理论上为2^32,即4G,有1G左右的空间是内核态,剩下的3G左右的空间是用户态。从高地址到低地址可以分为五个区域:
栈区:操作系统控制,存放局部变量,函数
堆区:程序员分配,动态变量
全局区/静态区:数据段,存放全局变量、静态变量
文字常量区:只读段,存放文字常量和全局常量 如
const char* p = "hello"
程序代码区:只读段,存放函数体的二进制代码
C风格字符串
C风格字符串即以空字符结尾的字符数组。
如果用数组形式保存字符串(字符数组,保存在栈区)。,注意留出终止符,当然也可以用上语法糖来初始化字符数组;
void test() {char str1[] = {'h', 'e', 'l', 'l', 'o', '\0'};char str2[] = "hello";cout << str1 << endl;cout << str2 << endl; }
如果用指针形式,直接定义为
const char *
(字符串常量,保存在文字常量区),C++代码中标准C风格字符串的写法。void test() {const char* pstr = "hello";// pstr[0] = 'H' // ERROR }
字符串常量存放在内存的只读区,如果用普通的char*指针保存其地址,可能会尝试通过指针修改内容,进而引发错误。如果用
const char *
指针,则可避免这种风险。注意:输出流运算符具有默认重载效果,接char*时会进行自动的访问,输出的是字符串内容,而不是地址。
练习
-
什么是命名空间?其作用是什么?匿名命名空间有什么特点?
-
const关键字与宏定义的区别是什么?(面试常考)
-
什么是常量指针和指向常量的指针?什么是数组指针和指针数组?什么是函数指针和指针函数?请举例说明
-
new/delete与malloc/free的区别是什么?(面试常考)
-
简述C++中引用与指针的联系与区别
-
什么是函数重载?其实现原理是什么?如何进行C与C++的混合编程?
-
什么是inline函数?inline与带参数的宏定义之间的区别是什么?
-
C++内存布局是怎样的?请简述
-
C语言中的struct与C++中的struct有什么区别?