C & C++基础

版权声明: https://blog.csdn.net/qq_41880190/article/details/88848297

static 关键字的作用

C 语言中 static 修饰局部变量、全局变量、函数

  • static 修饰的静态局部变量只执行一次,而且延长局部变量的生命周期
  • static 修饰全局变量,此全局变量只能在本文件中使用,即便外部声明也不可以
  • static 修饰一个函数,此函数只在本文件中使用,static 修饰的局部变量存放在全局数据区的静态变量区,自动初始化为 0

C++ 中修饰类 static 静态数据成员、静态成员函数

  • static 修饰静态数据成员,此成员可以实现多个对象之间的数据共享,它是类所有对象的共享成员,在内存中只占一份空间,如果改变它的值,所有对象中这个数据成员的值都被改变,其在程序开始运行时被分配空间,静态数据成员只能在类外进行初始化,静态数据成员可以通过对象名和类名引用
  • static 修饰成员函数,此成员函数属于类的成员函数,不属于对象,没有 this 指针,静态成员函数主要用于访问静态数据成员,非静态数据成员不能访问

C 和 C++ 的区别

函数默认值参数、inline,函数重载、引用、new 和 malloc、作用域

inline

inline只适用于函数体内代码简洁的函数使用,不能包含复杂的结构控制语句(循环 or 递归)内联函数仅仅是一个对编译器的建议,建议将 inline 放在头文件中(内联函数要在调用点展开,所以编译器必须随处可见内联函数的定义)小心代码膨胀

new 和 malloc

  • 他们都是动态管理内存的入口
  • malloc / free 是 标准库函数,new / delete 是操作符
  • malloc / free 只是动态分配内存空间 / 释放空间。而 new / delete 除了分配空间意外还会调用构造函数和析构函数进行初始化和清理(清理成员)
  • malloc / free 需要手动计算类型大小且返回值为 void* ,new / delete 可自己计算类型大小,返回对应的指针
  • malloc 失败了会返回 NULL ,new 失败了会抛异常;
  • malloc/free只能申请内置类型的空间,不能申请自定义类型的空间,因为其不会调用构造与析构函数, 而new可以,new在申请空间后会调用构造函数完成对象的构造,delete在释放空间前会调用析构函数完成空间中资源的清理

new(操作符)—>调用构造----->调用operator new(函数)—>调用malloc(函数)

new失败抛异常(符合C++规范)

malloc失败返回NULL

delete(操作符)–>调用析构—>调用operator delete(函数)—>free(函数)

C++ 类型转换(与强制类型转换区别)

是什么、怎么用、基本原理

C++中层次类型转换中无非两种:上行转换和下行转换

对于上行转换

static_cast和dynamic_cast效果一样,都安全;

对于下行转换

你必须确定要转换的数据确实是目标类型的数据,即需要注意要转换的父类类型指针是否真的指向子类对象,如果是,static_cast和dynamic_cast都能成功;如果不是static_cast能返回,但是不安全,可能会出现访问越界错误,而dynamic_cast在运行时类型检查过程中,判定该过程不能转换,返回NULL。

static_cast

编译时类型检查

用法

void Func()
{
    int i;
    float f;
    f = (float)i;
    f = static_cast<float>(i);
    想得到的类型 = static_cast<目标类型>(目标)
}

使用场景

  • 用于基本数据类型之间(将 int 转换为 char 会有截断,高八位直接丢弃,导致数据丢失)
  • 把空指针转换为目标类型的空指针
  • 把任何类型的表达式转化为 void 类型
  • 用于父类子类之间指针和引用的转换

用于父类子类之间的转换,存在两种形式即上行转换(子类到父类)和下行转换(父类到子类),前者安全,(因为子类总是包含父类的数据成员和函数成员,因此从子类转换到父类的指针的对象可以没有任何顾虑的访问其成员,)至于下行转换不安全,static_cast 转换成功返回转换后的正确类型,转换失败不报错,不反回 NULL

const_cast

dynamic_cast

运行时类型检查

用法

dynamic_cast<type_id>(expression)

使用场景

  • 主要运用于类层次结构中父类和子类引用和指针的转换,由于具有运行时类型检查因此可以保证下行转换的安全性转换失败返回 NULL

reinterpret_cast

C/C++ 指针和引用的区别

  • 指针可以为空,在使用指针前必须判空,而定义一个引用时必须初始化
  • 指针可以指向不同对象,而引用不可以改变指向
  • 引用在定义时,必须初始化,但是指针没有要求,但建议初始化;
  • 指针可以指向多个变量,引用类型一旦与变量绑定就不可更改;
  • 定义一个指针变量 p 时,++p偏移一个元素类型的大小,而 int a = 10; int &ra = a; ++ra则表示 在a的实体上加1;
  • 两者遇到sizeof的含义也不一样,指针的大小由平台和类型觉得,引用则取决于引用实体的大小;
  • 指针需要手动寻址,引用通过编译器自动寻址。

智能指针

是什么、怎么用、基本原理

auto_ptr

shared_ptr

数组和指针的区别

概念、赋值、存储方式、sizeof、初始化、传参、函数指针,数组指针,指针数组、

概念

数组:是用于存储多个相同类型数据的集合

指针:指针相当于一个变量,但是它和一般变量不一样,它存放的是其他变量在内存中的地址

赋值

同类型指针变量可以相互赋值,而数组只能一个一个赋值或拷贝

存储方式

数组:数组在内存上是连续存储的,开辟一块连续的内存空间,按照下表进行访问数组元素,多维数组也是按照一维数组进行组织的

指针:指针灵活,可以指向任意类型的数据,指针的类型说明了它所指地址空间的内存

sizeof

数组

  • 数组所占存储空间的内存:sizeof(数组名)
  • 数组的大小:sizeof(数组名)/sizeof(数据类型)

指针

指针大小与平台相关

野指针

指向不明确的指针变量,

如何避免

  • 养成良好的编码习惯
  • 当我们定义一个指针的时候直接让其为 NULL,NULL 的宏定义是 #define NULL (void*)0,代表零地址,而零地址是不能进行读写操作的
  • 要想给指针指向的空间赋初值时,必须给指针分配空间,可以使用 malloc 分配,成功后返回空间首地址
  • 使用 malloc 分配后,要检查是否分配成功
  • 分配好的空间记得清零,使用完毕进行 free,释放完毕让其指向 NULL

智能指针的内存泄漏问题、如何解决

为什么析构函数必须是虚函数、为什么 C++ 默认析构函数不是虚函数

多态是面向对象的一个基本属性,包括静态多态(编译阶段)和动态多态(运行阶段),静态多态主要是指函数参数不同产生的多态性,是在编译阶段可以识别的一种多态机制,而运行时多态则主要用于基类指针指向派生类对象时,可以通过基类指针直接调用派生类的对象函数,当然这种多态是通过虚函数实现的。

虚函数的目的就是通知系统在函数调用时能够自动识别对应的类对象类型,从而能够根据指针所指类型调用对应的类对象,实现函数调用时的多态性。对于析构函数而言,同样适用于上述规则。如果析构函数不是虚函数,那么在调用该函数时(对象被删除时)则只会调用当前对象对应的类的析构函数,这对于直接定义的对象是没有什么影响的,但是对于使用基类指向派生类的指针而言,因为基类指针实际上是基类类型,所以析构时自然只会调用基类的析构函数,这就可能产生内存泄漏(因为派生类的析构函数不被调用)。所以如果确定程序中有基类指针指向派生类的问题,则必须将基类的析构函数指定为虚函数,如此才能确保 NEW 出来的对象被正确的 DELETE。

构造函数&析构函数与虚函数的联系

静态函数和虚函数

重载、重写、覆盖

虚函数和多态

前置 ++ 和后置 ++

前置 ++ 返回引用,后置 ++ 返回 const 对象 & 后置 ++ 会生成临时对象

++a 表示取 a 的地址,增加他的内容,然后把值放入寄存器中

a++ 表示取 a 的地址,把他的值装入寄存器,然后增加内存中 a 的值(也就是说,操作时用的时寄存器中的值,增增前的值)

C++ 中如何定义常量

#define、const

  • 定义一个常量的时候,const 和 #define 都可以达到效果,但是一般采用 const,因为 #define 只是简单的符号代替,而 const 可以进行类型检查
  • 多个for()循环的时候,一般将循环次数少的放在外面,多的放在内层中,这样可以减少CPU的切换次数
  • enum 定义枚举常量,但是仅仅为整型

extern C

使用场景

  • C++ 中调用 C 语言代码
  • C++ 头文件中
  • 多人协同开发,有的人擅长 C++,有的人擅长 C 语言

extern C 主要作用就是为了让 C++ 代码调用其他 C 语言代码,加上 extern C 后,会指示编译器将这部分代码按照 C 语言的编码格式进行编译,

RTTI

是什么、基本实现

概念

RTTI 运行时类型识别,RTTI 是为了让程序在运行期间能根据基类的指针或引用来获得该指针或引用所指对象的实际类型

C++ 通过如下操作提供 RTTI

  • typeid 运算符,该运算返回其表达式或类型名的实际类型
  • dynamic_cast 运算符,该运算符将基类的指针或者引用安全的转换为派生类类型的指针或引用

基本实现

class type_info
{
public:
    virtual ~type_info();
    bool operator==(const type_info&)const;
    bool operator!=(const type_info&)const;
    bool before(const type_info&)const;
    const char* name()const;
private:
    type_info(const type_info&);
    type_info& operator=(const type_info&);
};

虚函数具体怎样实现运行时多态的

C 语言参数压栈顺序

参数压栈顺序从右至左,

C++ 如何处理返回值

printf 函数实现

拷贝构造函数的无穷递归问题

select、epoll

fork、exec、wait

strcpy、strlen

strcpy 的安全性问题

由于 strcpy 并没有保证目标字符串大小肯定大于源字符串大小,因此引入了一个 strcpy_s,其函数原型

car* strcpy_s(char* dst, int size, const char* src)

strcpy 实现

char* strcpy(char* dst, const char* src){
    if(dst == NULL || src == NULL){
        return NULL;
    }
    if(dst == src){
        return dst;
    }
    char* tmp = dst;
    while((*dst++ = *src++) != "\0");
    return tmp;
}

注意

  • 为了保护源字符串不被修改,传入的源字符串加 const 修饰
  • 检查字符串的指针是否有效,在 C 中检查字符串是否有效判断指针是否等于 NULL
  • 注意拷贝结束标志 “\0”,返回值为 char* 类型

strlen 实现

int strlen(const char* str){
    assert(str != NULL)
    int i = 0;
    int count = 0;
    while(str[i] != "\0"){
        count++;
        i++;
    }
    return count;
}
int strlen(const char* str){
    const char* p = str;
    while(*p != "\0"){
        p++;
    }
    return p - str;
}
int strlen(const char* str){
    if(*str == "\0"){
        return 0;
    }
    else{
        return 1 + strlen(str + 1);
    }
}

虚继承原理

是什么、解决了什么问题、怎么用、执行顺序、与虚函数的关系

什么是虚继承

虚继承,解决从不同途径继承而来的同名的数据成员在内存中有着不同的拷贝造成的数据不一致问题,将共同基类设置为虚基类。这时从不同途径继承过来的同名数据成员在内存中就只有一个拷贝,同一个函数名也只有一个映射

虚继承解决了什么问题

解决了菱形继承中的二义性问题,也节省了内存,避免了数据不一致问题

虚继承的语法

class A
{
    int g_val;
};
class B : virtual public A
{};
class C : public A
{}
class D : public B, public C
{};

执行顺序的优先级

  • 首先执行虚基类的构造函数,多个虚基类的构造函数按照被继承顺序来构造

  • 执行基类的构造函数,多个基类的构造函数按照被继承的顺序来构造

  • 执行成员对象的构造函数,多个成员对象按照被声明的顺序来构造

  • 执行派生类自己的构造函数

  • 析构函数以构造函数相反的顺序执行

从虚基类直接或间接的派生类中的构造函数的成员初始化列表中都要列出对虚基类构造函数的调用,但只有建立对象的最派生类的构造函数调用虚基类的构造函数,而该派生类的所有基类中列出的对虚基类的构造函数的调用在执行中被忽略,从而保证对虚基类子对象只初始化一次

优先级

在一个成员的初始化列表中同时出现对虚基类和非虚基类构造函数的调用时,虚基类的构造函数先于非虚基类的构造函数执行

虚基类的内存布局

class Base
{
public:
    double dou;
};
class Derived1 : public virtual Base
{
public:
	double in;
};
class Derived2 : public virtual Base
{
public:
	double on;
};
class A : public Derived1, public Derived2
{};

int main()
{
    int i = 0xaabbccdd;
    double a = 1, b = 2, c = 3, d = 4;
    Base bobj;
    bobj.dou = 1;
    Derived1 d1obj;
    d1obj.dou = 1;
    d1obj.in = 2;
    Derived2 d2obj;
	d2obj.dou = 3;
	d2obj.on = 1;
	A aobj;
	aobj.dou = 4;
	aobj.in = 1;
	aobj.on = 1;
	cout << sizeof(bobj) << endl;
	cout << sizeof(d1obj) << endl;
	cout << sizeof(d2obj) << endl;
	cout << sizeof(aobj) << endl;
    return 0;
}
8
20
20
32
  • 在虚继承体系中的派生类内存布局依次是:虚基类表指针,派生类本身的非 static 成员变量,继承至虚基类的非 static 成员变量。虚基类指针放在最前面,而从虚基类继承来的成员则在最后面
  • 类 A 的对象 aobj 中确实只持有一份虚基类的成员变量,并没有因同时继承了 Derived1 和 Derived2,而持有两份;那么,如果去掉虚继承,改为普通的继承,aobj 的内存布局又会是怎样呢?(去掉代码中的两个virtual 关键字,调试一下内存布局,可以发现 d1obj,d2obj,aobj 的内存空间没有虚基类表指针;如果在代码中用到 aobj.dou,会编译报错,说 “dou的访问不明确”,需要指明是从那个类继承来的 dou,例如:aobj.Derived::dou)。运行非虚继承的代码,可以发现输出结果是:8,16,16,32。原因是没有虚继承,也就没有虚基类指针,d1obj 和 d2obj 的大小变为 16 字节,而 aobj 的大小还是 32 字节,是因为它分别从Derived1 和 Derived2 继承了两份 dou。)
  • aobj 中会有两个虚基类指针,但编译器只通过其中一个决定虚基类 Base 的 dou 变量在 aobj 中的位置

虚基类表中存放了虚基类成员在派生类内存空间中的偏移量

虚函数

是什么、有什么作用、虚函数具体实现

什么是虚函数

简单来说,虚函数就是被 virtual 关键字修饰的成员函数,其作用为了实现多态特性,多态性就是将接口与实现分离;其实就是实现一种共同的方法,但因个体差异而采用不同的策略,虚函数主要有虚表实现

什么是纯虚函数

纯虚函数相当于基类只提供接口而不进行定义,在函数声明后面加上“=0”

例如:virtual void Eal() = 0;

虚函数与纯虚函数区别

纯虚函数只能在派生类中实现,因为它只提供了接口,而虚函数在派生类中可以覆盖也可以不覆盖,直接使用基类的实现

重载与重写(覆盖)、隐藏的区别

重载

  • 同一作用域
  • 函数名相同
  • 参数不同
  • 虚拟关键字可有可无

覆盖

派生类函数重写基类函数

  • 非同一作用域(分别位于派生类和基类)
  • 函数名相同
  • 参数相同
  • 基类函数必须有 virtual 关键字修饰

隐藏

派生类函数屏蔽基类同名函数

  • 如果派生类的函数与基类的函数同名,但是参数不同,此时,不论有无 virtual 关键字,基类的函数将被隐藏(注意不能与重载混淆)
  • 如果派生类的函数与基类的函数同名,但是参数不同,但是基类函数没有 virtual 关键字,此时基类的函数被隐藏(注意不能与覆盖混淆)

static 成员函数不能被覆盖和隐藏

class Base
{
public:
    virtual void func(float x)
    {
        cout << "Base::func(float)" << x << endl;
    }
    void gloub(float x)
    {
        cout << "Base::gloub(float)" << x << endl;
    }
    void hub(float x)
    {
        cout << "Base::hub(float)" << x << endl;
    }
};
class Derived : public Base
{
public:
	virtual void func(float x)
    {
    	cout << "Derived::func(float)" << x << endl;
    }
    void gloub(float x)
    {
    	cout << "Derived::gloub(float)" << x << endl;
    }
    void hub(float x)
    {
    	cout << "Derived::hub(float)" << x << endl;
    }
};

int main()
{
    return 0;
    Derived d;
    Base *pb = &d;
    Derived *pd = &d;
    pb->func(3.14f);	//覆盖
    pd->func(3.14f);	//覆盖
    pb->gloub(3.14f);	//非虚函数,直接调用基类函数
    pd->gloub(3.14f);	//隐藏了只能调用派生类函数
    pb->h(3.14f); //Base::h(float) 3.14(非虚函数,直接调用基类函数)
	pd->h(3.14f); //Derived::h(float) 3.14(隐藏)
}

虚函数实现运行时多态

猜你喜欢

转载自blog.csdn.net/qq_41880190/article/details/88848297