1 c++基础 -- 学习笔记

目录:


Linux操作系统下C++头文件路径:/usr/include/c++/…

一.命名空间namespace

  1. 基本格式…,可存放变量,常量,函数,结构体,类,模板,命名空间,都称为实体。

  2. 三种使用方式:

    using编译指令,作用域限定符,using申明机制

  3. 匿名命名空间

    ​ 和声明为static的全局名称的链接属性是相同的,即名称的作用域被限制在当前文件中,无法通过在另外的文件中使用extern声明来进行链接。命名空间都是具有external连接属性的,只是匿名的命名空间产生的__UNIQUE_NAME__在别的文件中无法得到,这个唯一的名字是不可见的。

  4. 命名空间的嵌套与覆盖,命名空间是可扩展的。

  5. 下面引用当前流行的名称空间使用指导原则:

    提倡在已命名的名称空间中定义变量,而不是直接定义外部全局变量或者静态全局变量。

    如果开发了一个函数库或者类库,提倡将其放在一个名称空间中。

    对于using 声明,首先将其作用域设置为局部而不是全局。

    不要在头文件中使用using编译指令,这样,使得可用名称变得模糊,容易出现二义性。 主要原因就是防止名字重复(即自定义变量名和命名空间中名字重复),因为头文件会被很多地方使用,你不知道这个using能覆盖多大范围。

    包含头文件的顺序可能会影响程序的行为,如果非要使用using编译指令,建议放在所有#include预编译指令后。


二.const关键字的用法

  1. const关键字修饰的变量称为常量,常量必须在定义时进行初始化,且之后不能为它赋值。

  2. 常考题:const常量与宏定义的区别是什么?

    • 编译器处理方式不同。宏定义是在预处理阶段展开,做字符串的替换;而const常量是在编译时才做解析。
    • 类型和安全检查不同。宏定义没有类型,不做任何类型检查;const常量有具体的类型,在编译期会执行类型检查。
    • 在使用中,应尽量以const替换宏定义,可以减小犯错误的概率。
  3. const关键字修饰指针,指针常量(const int *p)与常量指针(int * const p);

    类比C语言中的知识:

    函数指针:int (* pf)() 指针函数:int* func()

    数组指针:int (*pArray)[N] 指针数组:int * pArray[N]

  4. 修饰成员函数

  5. 修饰对象


三.new/delete表达式

  1. 常考题:new/delete表达式与malloc/free的区别是?

    1. malloc/free是C/C++语言的标准库函数,new/delete是C++的运算符或表达式 ;

    2. new能够自动分配空间大小,malloc需要传入参数;

    3. new开辟空间的同时还对空间做了初始化的操作,而malloc不行;

    4. new/delete能对对象进行构造和析构函数的调用,进而对内存进行更加详细的工作,而malloc/free不能。

    5. 既然new/delete的功能完全覆盖了malloc/free,为什么C++还保留malloc/free呢?

      因为C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存。

  2. malloc/free与new/delete之间的异同点?

    相同点:

    1. 都是用来申请堆空间
    2. malloc和free以及new与delete要成对出现,否则就会造成内存泄漏

    不同点:

    1. malloc/free是C里面的库函数,new/delete是C++的表达式
    2. malloc申请的是原始的未初始化的堆空间,new申请的是已经初始化的空间
  3. 什么是内存泄漏?内存溢出?内存踩踏?

    内存泄露: 内存泄露是指在堆中申请一块内存,但没有手动释放,导致指针消失,而指针指向的东西还在,但已经不能控制这块内存。

    内存溢出:内存溢出是指程序在申请内存时,没有足够的内存空间供其使用。

    内存踩踏: 内存踩踏(内存重叠)是指访问了不合法的地址(访问了不属于自己的地址),如果访问的地址是其他变量的地址,就会破坏别人的数据,从而导致程序运行异常。

  4. malloc底层是怎么实现?
      malloc基本的实现原理就是维护一个内存空闲链表,当申请内存空间时,搜索内存空闲链表,找到适配的空闲内存空间,然后将空间分割成两个内存块,一个变成分配块,一个变成新的空闲块。如果没有搜索到,那么就会用sbrk()才推进brk指针来申请内存空间。在已经映射的内存空间结尾有一个break指针,这个指针下面是映射好的内存,可以访问,上面则是未映射的访问,不能访问。可以通过系统调用sbrk(位移量)能移动brk指针的位置,同时返回brk指针的位置,达到申请内存的目。brk(void *addr)系统调用可以直接将brk设置为某个地址,成功返回0,不成功返回-1。而rlimit则是限制进程堆内存容量的指针。在操作系统角度来看,分配内存有两种方式,一种是采用推进brk指针来增加堆的有效区域来申请内存空间,还有一种是采用mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。这两种方式都是分配虚拟内存,只有当第一次访问虚拟地址空间时,操作系统给分配物理内存空间。


四.引用

  1. 注意事项:

    1. &在这里不再是取地址符号,而是引用符号,相当于&有了第二种用法 ;
    2. 引用的类型必须和其绑定的变量的类型相同 ;
    3. 声明引用的同时,必须对引用进行初始化;否则编译时报错 ;
    4. 一旦绑定到某个变量之后,就不会再改变其指向 。
  2. 引用的本质:
      C++中的引用本质上是一种被限制的指针。类似于线性表和栈的关系,栈是被限制的线性表,底层实现相同,只不过逻辑上的用法不同而已。 由于引用是被限制的指针,所以引用是占据内存的,占据的大小就是一个指针的大小。有很多的说法, 都说引用不会占据存储空间,其只是一个变量的别名,但这种说法并不准确。引用变量会占据存储空间,存放的是一个地址,但是编译器阻止对它本身的任何访问,从一而终总是指向初始的目标单元。在汇编里,引用的本质就是“间接寻址”。在后面学习了类之后,我们也能看到相关的用法。

  3. 引用作为函数参数

  4. 引用作为函数的返回值
      以引用作为函数的返回值时,返回的变量其生命周期一定是要大于函数的生命周期的,即当函数执行完毕时,返回的变量还存在。

  5. 注意:

    1. 不能返回局部变量的引用。主要原因是局部变量会在函数返回后被销毁,因此被返回的引用就成为了"无所指"的引用,程序会进入未知状态。
    2. 不能在函数内部返回new分配的堆空间变量的引用。如果返回的引用只是作为一个临时变量出现,而没有被赋予一个实际的变量,那么该引用所在的空间就无法释放,会造成内存泄漏。
  6. 引用总结:

    1. 在引用的使用中,单纯给某个变量取个别名是毫无意义的,引用的目的主要用于在函数参数传递中,解决大块数据或对象的传递效率和空间不如意的问题。
    2. 用引用传递函数的参数,能保证参数传递中不产生副本,提高传递的效率,且通过const的使用,保证了引用传递的安全性。
    3. 引用与指针的区别是,指针通过某个指针变量指向一个变量后,对它所指向的变量间接操作。程序中使用指针,程序的可读性差;而引用本身就是目标变量的别名,对引用的操作就是对目标变量的操作。
  7. 指针和引用的联系和区别

    联系:

    1. 都是地址的概念;
    2. 指针指向一块地址,它的内容是所指内容的地址;引用是某块内存的别名;

    区别:

    1. 指针是一个实体,而引用是别名;
    2. 引用定义时绑定后不能改变指向,而指针可以改变指向;
    3. 引用不能为空,指针可以为空;
    4. 引用符号后没有const,而指针后可以有const;
    5. 引用使用时无需解引用,而指针需要解引用;
    6. sizeof()引用和指针的含义不一样;
    7. ++引用和指针的含义不一样。

五.强制类型转换。

传统的c的类型转换不安全(因为可以进行任意类型之间的转换,比如const对象的指针转换为非const对象的指针,基类指针转换为派生类指针),且不易于查找。

  1. static_cast

    1. 用于基本数据类型之间的转换,如把int转成char,把int转成enum。这种转换的安全性也要开发人员来保证。 float fNumber = static_cast<float>(iNumber);
    2. 把void指针转换成目标类型的指针,但不安全。
    3. 把任何类型的表达式转换成void类型。
    4. 用于类层次结构中基类和派生类之间指针或引用的转换。进行上行转换(把派生类的指针或引用转换成基类指针或引用)是安全的;进行下行转换(把基类指针或引用转换成派生类指针或引用)时,由于没有动态类型检查,所以是不安全的。
  2. const_cast

    该运算符用来修改类型的const属性。常量指针被转化成非常量指针,并且仍然指向原来的对象;常量引用被转换成非常量引用,并且仍然指向原来的对象;常量对象被转换成非常量对象。

    const int number = 10;
    /* int *p = &number;//error */
    int *pInt = const_cast<int *>(&number);
    cout << "number = " << number << endl;
    cout << "*pInt = " << *pInt << endl;
    cout << "&number = " << &number << endl;
    cout << "&pInt = " << &pInt << endl;
    cout << "pInt = " << pInt << endl;
    
    cout << endl;
    *pInt = 200;//未定义的行为
    cout << "number = " << number << endl;
    cout << "*pInt = " << *pInt << endl;
    cout << "&number = " << &number << endl;
    cout << "&pInt = " << &pInt << endl;
    cout << "pInt = " << pInt << endl;
    

    对常量指针转化为非常量指针后赋值,是未定义的行为,会出现同个地址,打印出不同的结果。

  3. dynamic_cast
    该运算符主要用于基类和派生类间的转换,尤其是向下转型的用法中。

  4. reinterpret_cast

    ​   该运算符可以用来处理无关类型之间的转换,即用在任意指针(或引用)类型之间的转换,以及指针与足够大的整数类型之间的转换。由此可以看出,reinterpret_cast的效果很强大,但错误的使用reinterpret_cast很容易导致程序的不安全,只有将转换后的类型值转换回到其原始类型,这样才是正确使用reinterpret_cast方式。


六.函数重载

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

  1. C++进行函数重载的实现原理叫名字改编(name mangling),具体的规则是:

    1. 函数名称必须相同 。
    2. 参数列表必须不同(参数的类型不同、个数不同、顺序不同),满足一项即可。
    3. 函数的返回类型可以相同也可以不相同。
    4. 仅仅返回类型不同不足以成为函数的重载。
  2. 具体原理:对函数名字做了改编(name mangling)  改编的步骤:在同一个作用域中,当函数名字相同的时候,会按照参数个数、参数顺序、参数类型进行改编。

  3. 可以用g++ -c来编译程序,生成*.o文件,使用nm *.o查看符号表,来看具体指定的.o文件中函数重载后的名字(改编后)

  4. C++调用C函数时,不会对C函数进行名字改编

  5. C++中若要对某个代码段用C的方式进行编译,用extern “c” { }包起来即可

  6. 想将一段代码,既可以放在C++中进行编译,又可以放在C中编译,且都以C的方式进行编译

    #ifdef __cplusplus //C与C++的混合编程
    extern "C"
    {
    #endif
    
    int add(int x, int y)
    {
        return x + y;
    }
    
    #ifdef __cplusplus
    }        //end of extern C
    #endif
    
    //原因是__cplusplus该宏定义C++可以识别,而C不能识别
    
  7. extern的其他用法
      在一个c++项目中,一个A.cc文件中定义了一个变量和函数,而B.cc想要使用,就可以在B.cc中用extern来修饰该变量和函数进行一次声明,这样申明后,在B.cc里就可以使用A.cc中定义的该变量和函数,需要注意的是,能被成功引用的原因是A中定义的变量和函数本身是具有外链接(external)属性的,而不是extern本身,而且extern申明所在的作用域,将是引用成功后的变量和函数的作用域。

    //A.cc
    #include <iostream>
    int v = 1;
    void fun()
    {
    	std::cout << "hello world" << std::endl;
    }
    
    //B.cc
    #include <iostream>
    using namespace std;
    extern int v;
    extern void fun();
    int main()
    {
    	cout << v<< endl;
    	fun();
    	return 0;
    }
    

七.默认参数

默认参数赋值时必须从右边往左边连续进行赋值

原因:函数调用时参数是从右向左进行入栈,比如当参数是两个非引用的对象,则右边参数的对象的拷贝构造函数先进行调用。

原则:不能产生二义性


八.bool类型

占用一个字节


九.内联函数

具备宏的效率,又增加了安全性。

  1. 内联:编译器将使用函数的定义体来替代函数调用语句,这种替代行为发生在编译阶段而非程序运行阶段。

  2. 内联函数不能分成头文件与实现文件的形式(不能将声明与实现分开),不然就会报错

    如果一定要将头文件与实现文件分开,可以在头文件中包含实现文件#include实现文件

  3. 注意:内联函数可以称为小函数,一般函数的大小比较小不要有for/while等复杂的语句,更不要在内联函数中进行函数嵌套使用。(太多内联代码拷贝过多,占内存,且如果执行函数体内代码的时间比函数调用的开销大的多,此时内联也没有太大的效率收益)

  4. 内联函数与带参数的宏定义的区别

    1. 使用宏定义容易出错;
    2. 宏定义不可调试,内联函数可以调试;(参考函数内联如何工作[符号表])
    3. 宏定义无法操作类的私有数据成员;
    4. 函数被内联后,编译器可以通过上下文相关的优化技术对结果代码进行更深入的优化。

或者这个答案:

  1. 在预处理时期,宏定义在调用处执行字符串的原样替换(宏展开)。在编译时期,内联函数在调用处展开,同时进行参数类型检查,宏定义不会进行参数类型检查;
  2. 内联函数首先是函数,可以像调用普通函数一样调用内联函数。而宏定义往往需要添加很多括号防止歧义,编写更加复杂;
  3. 内联函数可以作为某个类的成员函数,这样可以使用类的保护成员和私有成员。而当一个表达式涉及到类保护成员或私有成员时,宏就不能实现了(无法将this指针放在合适位置);
  4. 可以用内联函数完全替代宏。

十.异常安全

1.try throw catch


十一.C和C++风格字符串

(https://en.cppreference.com)可下载下来用everything软件查找[或者中文网站:zh.cppreference.com]

  1. C风格字符串的定义形式:

    字符数组char str1[] = “hello”; //不能改变当前字符数组名(指针)的指向,但可以改变值

    字符指针const char *pstr = “helloworld”; //可以改变指针的指向,但是不能改变值

  2. C++字符串:string

    C++风格字符串转为C风格:string.c_str()

    获取长度:string.size(),string.length()

    遍历C++风格字符串:利用下标

    拼接:“+”,string.append()


十二.程序内存分配方式

1.内存分配

  1. 栈区 有编译器进行分配和释放,函数参数、局部变量

  2. 堆区 由程序员进行分配与释放,malloc、new申请的都是堆空间,需要用free与delete进行手动释放,可能由OS回收

  3. 读写段:全局变量,静态变量

  4. 只读段:文字常量区 存放字符串常量, 程序代码区 存放程序的二进制代码
    请添加图片描述

int a;//全局变量,位于全局区,默认初始化为0
char *p1;//全局变量,位于全局区, 默认初始化为NULL
const int kMax = 100;//只读段,文字常量区

int main(int argc, char **argv)
{
    const int kMin = 0;//栈区
    int b;//栈区,默认是随机值
    char *p2;//栈区
    static int c = 10;//静态变量,位于静态区
    char str1[] = "hello,world";//{'h', 'e', 'l','l'}

    int *pInt = new int(10);//pInt本身位于栈上,pInt指向堆区

    const char *pstr = "hello,world";//sptr本身位于栈上,
                                     //指向的区域位于文字常量区
    
    printf("\"hello,world\" = %p\n", &"hello,world");//字符串常量
    
    printf("&main = %p\n", &main);
    printf("main = %p\n", main);//程序代码区
}

2.全局变量int型默认初始化为0,指针类型默认初始化为NULL

3.栈跟堆的区别

  1. 管理方式不同。对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak.

  2. 空间大小不同。一般来讲在32位系统下,内存可以达到4G的空间,从这个角度 来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VS下,默认的栈空间大小是1M

  3. 分配方式。内存有2种分配方式:静态分配和动态分配。堆都是动态分配的,没有静态分配的堆,动态分配由malloc, calloc函数进行分配。栈是静态分配,静态分配是编译器完成的,比如局部变量的分配。

  4. 生长方向。对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。

  5. 碎片问题。对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在它上面的后进的栈内容已经被弹出。


十三. 一些重要的面试知识点

  1. static关键字的作用

    全局静态变量:位于静态存储区,在main函数之前构造,在整个程序结束销毁,被static修饰的全局变量对外部文件不可见,没有external属性。

    局部静态变量:位于静态存储区,当第一次使用的时候才进行构造,在局部作用域访问,离开局部作用域之后static变量仍然存在,但是不能访问。

    静态函数:函数默认情况下是有external属性,是可导出的,加了static就不能被外部类访问。注意不要在头文件中声明static函数,因为static只对本文件有效。

    类的静态成员:可以实现多个不同的类实例(对象)之间的数据共享,且不破坏隐藏规则,需要类外进行初始化。

    类的静态函数:不能访问非静态成员,没有this指针,可以通过对象.成员函数()和类名::函数名()调用。

    static变量不需要初始化,默认值为0。(存储在全局静态区的所有数据默认初始化为0,包括静态变量,全局变量, 因为在静态数据区,内存中所有的字节默认值都是0x00 )

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值