C++基础知识

一、基础知识

1.C++和C的联系与区别

C/C++ 的联系:

  • C++是C的超集,兼容大部分C的语法结构

C/C++ 的区别:

  • C是面向过程的语言,C++是面向对象的语言。面向对象的特点是封装、继承、多态。C++有继承和多态而C没有。
  • C和C++动态管理内存的方法不一样,C使用malloc/free,而C++除此之外还可以使用new/delete。
  • 接下来是类的不同,C中只有struct,其默认成员是public的,而C++不但兼容struct,还拥有C中没有的class,class成员默认是private的,但是支持public、private、protected三种运算符。
  • C++支持函数重载和运算符重载,而C不支持。
  • C++支持范式编程,拨入模板类,函数模板等
  • C++有引用而C没有,一些关键字是C++独有的,比如extern。

malloc和new的不同:

  • molloc函数声明(函数原型):void malloc(int size);
    说明:malloc 向系统申请分配指定size个字节的内存空间。返回类型是 void
    类型。void* 表示未确定类型的指针。C,C++规定,void* 类型可以强制转换为任何其它类型的指针。
    从函数声明上可以看出。malloc 和 new 至少有两个不同: new 返回指定类型的指针,并且可以自动计算所需要大小。
  int *p;
  p = new int; //返回类型为int* 类型(整数型指针),分配大小为 sizeof(int);
  p = (int *) malloc (sizeof(int));

第一、malloc 函数返回的是 void * 类型,如果你写成:p = malloc (sizeof(int)); 则程序无法通过编译,报错:“不能将 void* 赋值给 int * 类型变量”。所以必须通过 (int *) 来将强制转换。

第二、new不仅分配一段内存,而且会调用构造函数,而malloc不会
第三、函数的实参为 sizeof(int) ,用于指明一个整型数据需要的大小。如果你写成:

int* p = (int *) malloc (1);

代码也能通过编译,但事实上只分配了1个字节大小的内存空间,当你往里头存入一个整数,就会有3个字节无家可归,而直接“住进邻居家”!造成的结果是后面的内存中原有数据内容全部被清空。

malloc 也可以达到 new [] 的效果,申请出一段连续的内存,方法无非是指定你所需要内存大小。

比如想分配100个int类型的空间:

int* p = (int ) malloc ( sizeof(int) 100 ); //分配可以放得下100个整数的内存空间。

另外有一点不能直接看出的区别是,malloc 只管分配内存,并不能对所得的内存进行初始化,所以得到的一片新内存中,其值将是随机的。

除了分配及最后释放的方法不一样以外,通过malloc或new得到指针,在其它操作上保持一致。

总结:

malloc()函数其实就在内存中找一片指定大小的空间,然后将这个空间的首地址范围给一个指针变量,这里的指针变量可以是一个单独的指针,也可以是一个数组的首地址,这要看malloc()函数中参数size的具体内容。我们这里malloc分配的内存空间在逻辑上连续的,而在物理上可以连续也可以不连续。对于我们程序员来说,我们关注的是逻辑上的连续,因为操作系统会帮我们安排内存分配,所以我们使用起来就可以当做是连续的。

2.C++中的堆和栈

预备知识:程序的内存分配
一个由C/C++编译的程序占用的内存分为以下几个部分:

  • 栈区:由编译器自动分配释放,存放函数的参数值,局部变量的值等,栈区向低地址生长。(具体操作方式类似于数据结构中的栈)
  • 堆区:一般由程序员分配释放(即程序员自己申请分配的内存都是在堆中申请的),若程序员不释放,程序结束时可能由操作系统回收,堆区向高地址生长。(与数据机构中的堆是两回事,分配方式类似于链表)
  • 全局区(静态区):存放全局变量和静态变量,程序结束后由系统释放。全局变量和静态变量是放在一块的,初始化后全局变量和静态变量放在一块区域(.data),未初始化的全局变量和静态变量放在相邻的另一块区域(.bss)。
  • 程序代码区:存放函数的二进制代码。

const修饰的变量在内存中的位置:没有static修饰时放在栈区,有static修饰时放在全局区。

参考链接:https://blog.csdn.net/yingms/article/details/53188974

3.C++源文件到可执行文件经历的过程

①预处理器:对源代码进行头文件展开、宏替换、去掉注释,生成预编译文件。

②编译器:检查语法错误,将预编译文件转换成特定汇编代码,生成汇编文件.s

③汇编器:将汇编文件转化成二进制文件.o

④链接器:将多个目标文件及所需要的库连接成最终的可执行目标文件

4.如何在C++中调用C程序

C++和C是两种完全不同的编译链接处理方式,如果直接在C++里面调用C函数,会找不到函数体,报链接错误。要解决这个问题,就要在 C++文件里面显示声明一下哪些函数是C写的,要用C的方式来处理。
方法1.引用头文件前需要加上 extern “C”,如果引用多个,那么就如下所示 :

extern “C” 
{ 
     #include “ s.h” 
     #include “t.h” 
     #include “g.h” 
     #include “j.h” 
}; 

方法2.在c代码的头文件(比如上述的s.h)声明的函数前面加上extern “C”

#ifdef __cplusplus        //如果编译环境是C++
extern "C" {
#endif

void fun1(int arg1); 
void fun2(int arg1, int arg2); 
void fun3(int arg1, int arg2, int arg3);

#ifdef __cplusplus 
}
#endif

二、基础语法

1.extern的使用

extern:声明使得变量为程序所知

  • 在头文件里声明变量或函数,在多个源文件里共享
_extern.h里    extern int a;
fun.cpp里      int a=13;
main1.cpp里    cout<<a<<endl;
main2.cpp里    a=15;  cout<<a<<endl;
输出: a=13.
      a=15.
  • 在一个源文件里面定义,另外一个源文件想要使用时可以用extern进行声明
cpp1:    int b=15;
cpp2:   extern int b;  cout<<b<<endl;
输出:    b=15.

2.指针和引用的区别

  • 指针有自己的地址空间,而引用只是变量的别名。
  • sizeof的结果不同,指针的结果是4,引用的结果是被引用对象的大小。
  • 作为参数传递时,指针需要被解引用才能对对象进行操作,而对引用的修改会直接改变引用所指向对象的值。
  • 有const pointer而没有const reference。
  • 指针在使用时可以指向其他对象,但是引用只能是一个对象的引用,不能被改变。

3.顶层const和底层const

顶层const : 指针本身是个常量

(只有const pointer、const reference不存在)

  • const pointer(常量指针),p总是指向最初所指的那个对象
int num=0;
int *const p=&num;       //常量指针,指针p本身是个常量,只能一直指向num

底层const : 引用或指针所指的对象是个常量(即不能通过该引用或指针改变指向的对象的值)

(pointer to const 、 reference to const)

  • pointer to const(指向常量的指针):可以指向非const常量,但是不能通过p改变指向对象的值。
  • reference to const(指向常量的引用):可以指向非常量,但是不能通过该引用改变指向对象的值。
int num=42;
const int &r=num;        //r是一个指向常量的引用(但是可以用于指向非常量)
const int *p=num;        //p是一个指向常量的指针(但是可以用于指向非常量)
 
r=43;         //错误,r不能用于改变num的值
*p=43;       //错误,p不能用于改变num的值

指向常量的指针不能赋值给普通的指针:(必须要有相同的底层const)

const int a = 2; 
const int *p1 = &a;  //底层const,p1为指向常量的指针,不能通过p1改变a
 
int *p2 = &a;       //错误,应使用底层const,const int *p2 = &a
p2 = p1;            //错误,p1是底层const而p2不是,如果该句成立,将可以使用*p2来改变常量a的值,
                      这显然是错误的。

4.static的使用

1)全局静态变量:在全局变量前面加上static

  • 静态(全局)存储区
  • 作用域:声明它的整个文件可见,其他文件不可见

2)局部静态变量:在局部变量前面加上static

  • 静态(全局)存储区
  • 作用域:作用域为定义它的函数,但函数结束后并未被销毁只是不可访问,直到该函数再次被调用,此时局部静态变量的值为上一次函数调用时被保存的值。

3)静态函数:在函数的返回类型前面加上static

  • 函数的定义和声明默认都是extern的,但是静态函数只在声明它的.cpp文件可用,其他文件不可用,但是不会与其他.cpp文件中的同名函数引起冲突。

4)类的静态数据成员

  • 静态存储区
  • 类的静态成员不属于某个特定的类对象,而是所有类对象共享。
  • 类的静态数据成员可以在类内声明,但只能在类外定义和初始化。

5)类的静态成员函数

  • 类的静态函数可以直接引用类的静态成员,但是不能直接引用类中的非静态成员。如果要引用类的非静态成员,只能通过类对象来引用。
  • 类的静态成员函数不能声明为const,也不能使用this指针。

三、类相关

1.构造函数和析构函数

①构造函数和析构函数的调用顺序

  • 当派生类对象构造时,先调用基类的构造函数,然后调用派生类的构造函数
  • 当派生类对象析构时,先调用派生类的析构函数,然后调用基类的析构函数

②构造函数和析构函数可以是虚函数吗

构造函数不能是虚函数,析构函数一般且常常是虚函数
构造函数不能是虚函数:

  • 虚函数是通过指向虚函数表的虚函数指针vptr调用的,调用时要求已经通过构造函数完成了对类对象的初始化,包括vptr的初始化。若构造函数是虚函数,调用构造函数就需要通过vptr,但此时构造函数未被初始化。
  • 从多态的角度来讲,虚函数主要是为了实现多态,在运行时才能根据指针或引用指向的对象决定调用虚函数的版本。而构造函数是在创建类对象的时候自动调用类中相应的版本,无法实现多态。

析构函数一般都是虚函数:若存在类的继承关系,则析构函数必为虚函数,否则会发生内存泄漏

Base *p=new Drived();
delete p;
p=nullptr;
  • 若基类的析构函数不是虚函数:此时只调用基类的析构函数,基类对象和派生类对象的基类部分被释放,派生类对象的派生部分没有释放。
  • 若基类的析构函数时虚函数:基类和派生类对象都被释放(先调用派生类析构函数,再调用基类析构函数)。

③构造函数和析构函数可以抛出异常吗

构造函数可以,析构函数不行(或者析构函数不建议抛出异常)

  • 构造函数抛出异常:会导致析构函数无法被调用,但类对象本身已申请到的内存资源会被系统释放(已申请到资源的成员变量会被系统一次逆序调用其析构函数),因为析构函数不能被调用,所以可能会造成系统资源未被释放,即内存泄漏。
  • 析构函数抛出异常:内存泄漏,所以析构函数不能也不建议抛出异常。

2.一个空class里有什么

1)默认构造函数
2)析构函数
3)拷贝构造函数
4)拷贝赋值运算符
5)取址运算符
5)取址运算符const
以上函数都是public且是inline函数

class Empty
{
    public:
        Empty(); // 缺省构造函数
        Empty( const Empty& ); // 拷贝构造函数
        ~Empty(); // 析构函数
        Empty& operator=( const Empty& ); // 赋值运算符
        Empty* operator&(); // 取址运算符
        const Empty* operator&() const; // 取址运算符 const
}

3.继承和多态

①虚函数的作用

  • 对于某些函数,基类希望它的派生类定义适合自己的版本,此时基类将其定义为虚函数
  • 支持多态:当时用基类指针或引用调用虚函数时,根据指针或引用指向的对象不通,该调用既可能调用基类版本的虚函数,也可能调用派生类版本的虚函数。

②纯虚函数与抽象基类

纯虚函数:基类对纯虚函数没有定义,只希望派生类各自定义适合自己的版本
定义方式:在函数体的位置(记载声明语句的分号之前)加上=0;

class Base
{
public:
	int fun(int para) const=0;
};

抽象基类:含有纯虚函数的类是抽象基类
抽象基类无法创建类对象,只能被派生类继承。

③多态

  • 静态多态(函数重载和运算符重载):在编译时确定要调用的版本
  • 动态多态:动态绑定,在运行时确定要调用函数的版本。基类的指针或引用调用虚函数时,若指针或引用指向基类对象,则调用基类版本的虚函数,若指针或引用指向派生类对象,则调用派生类版本的虚函数。

④override和final

  • override(覆盖):显性地说明派生类中对基类虚函数的重新定义
    (派生类若定义了一个函数与基类函数中的虚函数名字相同但是参数列表不同,这仍是合法的,编译器认为派生类新定义的这个函数与基类原有的同名函数相互独立,此时派生类的函数并没有覆盖基类的版本,这种声明在实际的编程当中往往认为发生了错误,但是编译器并不会报错,这种错误情况可以使用override显性地声明对基类虚函数的覆盖来排错。)
struct B
{
    virtual void f1(int) const;
    virtual void f2();
    void f3();
};
 
struct D1:B()
{
    void f1(int) const override;    //正确
    void f2(int) override;          //错误,参数不同
    void f3() override;             //错误,f3不是虚函数
    void f4() override;             //错误,f4不存在
};
  • final:派生类中表示此函数不能再被其他派生类所覆盖
  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值