命名空间

即可由程序员自定义的作用域,域中的定义,在各作用域之间互不干扰。

作用是解决命名冲突

匿名命名空间不用给作用域起名

const

宏定义是在预处理时做字符串的直接替换,不做类型检查。

const赋予只读属性,有具体的类型,在编译时会做类型检查

指针xx与xx指针

  1. 常量
    • 指针常量(pointer to const):
      • int const * xxx const int * xxx
      • 无法修改该指针指向的值,但能修改指向
    • 常量指针
      • int * const
      • 可以修改指向的值,不能修改指向
  2. 数组
    • 数组指针
      • int arr[5] = {1,2,3,4,5};
        //arr代表数组首元素地址
        //&arr代表数组的首地址,虽然与数组首元素地址相同,但实际上单位为整个数组,即&arr+1是整个数组后面一位的地址
        int (*p)[5] = &arr;//数组指针
        //(*p)[]可取出数组的元素
        
    • 指针数组

      • 存储指针的数组
  3. 函数

    • 函数指针

      • void func(){}
        int main(){
        void (*func_ptr)() = &func;
        func_ptr();//即通过函数指针调用函数。}		
        
    • 指针函数

      • 返回值为指针的函数。可以作用于指向全局变量(不然指向局部变量,函数结束则发生未定义行为)

new/delete 与malloc/free

  1. new/delete是操作符,而malloc/free是库函数。

  2. new返回值为对应类型的指针,malloc返回值为void*

  3. new可以直接初始化,malloc申请的空间有脏数据。

  4. malloc参数为字节数,new则不需要传递大小。

引用(最重点)

概念

引用是一个已定义变量别名

语法:

int num = 2;
int & ref = num;

注意:

  1. &在这里不是取地址符号,而是引用符号(有个空格)
  2. 引用类型需与与其绑定的变量的类型相同(学习继承后则有变动)
  3. 声明引用的同时必须初始化,否则报错
  4. 引用一经绑定,无法更改绑定

本质

C++的引用实际上是一种被限制的指针

通常的说法中,引用不占据内存,只是一个变量的别名,但从原理理解,引用底层也是由指针实现的。所以也占据一个指针的大小。

对该引用取地址,获取到的就是绑定变量的地址

引用与指针的联系与区别?

联系:

  1. 引用与指针都是用来间接访问变量
  2. 引用的底层仍是指针,可以视为一个受限制的指针

区别

  1. 引用必须初始化,指针可以不初始化
  2. 引用不能修改绑定,但是指针可以修改指向;
  3. 在代码层面对引用本身取址取到的是绑定变量的地址,但是对指针取址取到的是指针变量的地址。

使用场景

作为函数的参数(重点)

学习引用之前,若想通过形参(函数的参数)改变实参的值,只能通过指针才能达到目的。引用则比指针更不容易犯错。<span style=color:red;background:yellow>在C++中推荐使用引用而非指针作为函数的参数。</span>

//在实参传给swap3时,如swap3(a,b)
//其实就是发生了初始化int & x = a与int & y = b;
void swap3(int & x, int & y){//引用传递,不复制
    int temp = x;
    x = y;
    y = temp;
}

注意事项

  1. 不要返回局部变量的引用。因为局部变量会在函数返回后被销毁,被返回的引用就成为了”无所指”的引用,程序会进入未知状态。
int & func()
{
	int number = 1;
    return number;
}
  1. <span style=color:red;background:yellow>不要轻易</span>返回一个堆空间变量的引用,非常容易造成内存泄漏。
int & func()
{
	int * pint = new int(1);
	return *pint;
}
void test()
{
	int a = 2, b = 4;
	int c = a + func() + b;//内存泄漏,没释放呢
    
}
-----------------------------------------------
int & func3(){
    int *p = new int(10);
    return *p;
}
void test1(){
    int & ref = func3();//借刀杀人,这样处理才行,此行会执行一次func3
    cout<< ref <<endl;//这些不会执行
    cout<< &ref <<endl;
    delete &ref;
}

总结

  1. 使用引用时,如果只是单纯给某个变量取别名,那没什么意义。主要目的在于函数的参数传递中,解决大块数据对象传递效率空间不理想的问题。

  2. 用引用传递函数的参数,能保证参数传递中不产生副本,提高传递的效率,还可以通过const的使用,保证了引用传递的安全性。

  3. 引用与指针的区别:

    • 指针是通过某个指针变量指向一个变量后,对它所指向的变量间接操作。主要还是因为使用指针难以看懂,程序可读性差。
    • 引用的底层仍是指针,但编译器不允许直接访问该底层指针。逻辑上可直接理解为对引用的操作,各方面都是对绑定变量的操作。**可以用指针或引用解决的问题,更推荐使用引用**

强制转换

C语言风格的强转有不少缺点

TYPE a = TYPEEXPRESSION;
  1. 它可以在任意类型转换,比如把一个指向const对象的指针,转换为非const对象指针,转前转后简直不是一个物种了,这并不合适。
  2. 不容易查找,如(int)xx这样的形式,在c++程序中寻找如大海捞针。

所以c++为了克服这些缺点,引进了4个新的类型操作转换符(cast=铸造)

static_cast;
const_cast;
dynamic_cast;
reinterpret_cast;

static_cast

最常用,使用形式:

目标类型 转换后的变量 = static_cast<目标类型>(要转换的变量)

例子:

int inum = 100;
float fnum = 0;
fnum = static_cast<float>(inum);

void*指针转换成其他类型也可以:

void *pVoid = malloc(sizeof(int));
int *pInt = static_cast<int*>(pVoid)

但不允许两个指针类型之间转换,如

int i = 1;
int *pi = &i;
float *f = static_cast<float *>(i)
// clang: Static_cast from 'int *' to 'float *' is not allowed

好处:不允许非法的转换,且方便查找。

const_cast

修改类型的const属性,基本不用

指向常量的指针被转化成普通指针,并且仍然指向原来的对象;

常量引用被转换成非常量引用,并且仍然指向原来的对象;

const int number = 100;
int * pInt = &number;//error
int * pInt2 = const_cast<int *>(&number);

dynamic_cast

该运算符主要用于基类和派生类间的转换,尤其是向下转型的用法中(后面讲)

reinterpret_cast

功能强大,比static_cast的使用范围更广一些,慎用(也称为万能转换

它可以用来处理无关类型之间的转换,即用在任意指针(或引用)类型之间的转换,以及指针与足够大的整数类型之间的转换。

由此可以看出,reinterpret_cast的效果很强大,但错误的使用reinterpret_cast很容易导致程序的不安全,**将转换后的类型值转换回到其原始类型,才是reinterpret_cast最正确的使用方式**

函数重载

在c++中允许多个函数拥有相同的名字,只要它们的参数列表不同就可以,这就是函数重载。

**在同一作用域内,可以有一组具有相同函数名,不同参数列表的函数,这组函数被称为重载函数。**

实现函数重载的条件

函数参数的数量、类型、顺序任一不同则可以构成重载。

只有返回类型不同,参数完全相同,是不能构成重载的

函数重载的实现原理

实际上是通过名字改编实现,即给函数加入参数类型后缀,如add(int a,int b)变为addii

extern “C”

如果在C++代码中想要对部分内容按照C的方式编译

extern "C" void func() //用 extern"C"修饰单个函数
{

}

//如果是多个函数都希望用C的方式编译
//或是需要使用C语言的库文件
//都可以放到如下{}中
extern "C"
{
//……
}

默认参数

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();
}

<span style=color:red;background:yellow>通常是将默认值的设置放在声明中而不是定义</span>

默认参数应从右至左逐渐定义。当调用函数时从左向右匹配参数

bool

任何非零值都将转换为true,而零值转换为false(<span style=color:red;background:yellow>注意:-1也是代表true</span>)

inline函数

**—— 在普通函数定义之前加上inline关键字**

(1)inline是一个建议,并不是强制性的

(2)inline的建议如果有效,就会在<span style=color:red;background:yellow>编译时</span>展开,可以理解为是一种更高级的代码替换机制

(3)函数体内容如果太长或者有循环之类的结构,不建议inline,以免造成代码膨胀;比较短小并且比较常用的代码适合用inline。

比如函数体中有循环结构,那么执行函数体的开销比调用函数的开销大得多,设为内联函数只能减少函数调用的开销,没有太大意义。

<span style=color:red;background:yellow>对比总结:</span>

  • 宏函数

    优点:只是进行字符串的替换,并没有函数的开销,对于比较短小的代码适合使用;

    缺点:没有类型检查,存在安全隐患,而且比较容易写错。

  • 普通函数

    优点:可调试,有类型检查,比宏函数更安全;

    缺点:函数的调用会增加开销。

  • 内联函数

    既具备宏代码的效率,又增加了安全性,**所以在C++中应尽可能的用内联函数取代宏函数。**

内联函数注意事项

  1. 内联函数采用声明和实现分离的写法

    inline int func(int a, int b);
       
    int func(...){...;}
    
  2. **如果要把inline函数声明在头文件中,则必须把函数定义也写在头文件中。**

    若头文件中只有声明没有实现,被认为是没有定义替换规则。

    如下,foo函数不能成为内联函数:

    inline void foo(int x, int y);//该语句在头文件中
       
    void foo(int x, int y)//实现在.cpp文件中
    { //... }
    
    **inline函数在头文件必须有定义。**
  3. 谨慎使用内联

    以下情况不宜使用内联:

    • 如果函数体内的代码比较长,使用内联将导致可执行代码膨胀过大。

    • 如果函数体内出现循环或其他复杂的控制结构,那么执行函数体内代码的时间将比函数调用开销大得多,因此内联的意义并不大。

异常处理

C++ 异常处理涉及到三个关键字:try、catch、throw.

注意:**catch的是类型,不是具体信息**

if(xxx){
    throw "xxx";
}

if(yyy){
    throw 1;
}

try {
//语句块
} catch(const char *) {
//具体的异常处理...
} ...
catch(double x) {//类型
//具体的异常处理...
}

内存布局(重要)

64位系统,理论空间达到16EB(2^64),但是受硬件限制,并不会达到这么多;

以32位系统为例,一个进程在执行时,能够访问的空间是虚拟地址空间。理论上为2^32,即4G,有1G左右的空间是内核态,剩下的3G左右的空间是用户态。从高地址到低地址可以分为五个区域:

  • 栈区:操作系统控制,由高地址向低地址生长,编译器做了优化,显示地址时栈区和其他区域保持一致的方向。(同一个区域,先定义的内容在较低的地址)

  • 堆区:程序员分配,由低地址向高地址生长,堆区与栈区没有明确的界限。

  • 全局/静态区:读写段(数据段),存放全局变量、静态变量。

  • 文字常量区:只读段,存放程序中直接使用的文字常量和全局的常量,如const char * p = “hello”; hello这个内容就存在文字常量区。

  • 程序代码区:只读段,存放函数体的二进制代码。

image-20240918105335848

C风格字符串

C风格字符串即以空字符结尾的字符数组。

  • 如果用数组形式保存字符串,注意留出终止符,当然也可以用上语法糖来初始化字符数组

    void test0(){
        char str1[] = {'h','e','\0'};
        char str2[] = "world"
    }
    
  • 如果用指针形式,直接定义为const char * ,C++代码中标准C风格字符串的写法。

    const char * pstr = "hello";
    pstr[0] = 'h'//error,不给改
    
  • <span style=color:red;background:yellow>注意:</span>输出流运算符具有默认重载效果,cout«char*时会进行自动的访问,输出的是字符串内容,而不是地址。

更新时间: