文章目录
1.final和override关键字
final和override是C++11引入的关键字,用于在类的继承和虚函数重写中进行特定的语义声明
-
final
final关键字用于修饰类、成员函数或虚函数,表示它们是最终版本,不允许被继承、重写或覆盖
用于修饰类时,表示该类的最终类,不能被其他类继承
用于修饰成员函数时,表示该成员函数是最终版本,不能被子类的同名函数重写或覆盖。
class Base {
public:
virtual void foo() final; // 声明foo()是最终版本,不允许子类重写
};
class Derived : public Base {
public:
// 试图重写foo(),但由于在Base中声明为final,将会产生编译错误
// void foo() override; // 错误,不能重写final函数
};
-
override
override关键字用于修饰派生类中覆盖基类的虚函数,用于明确标识该函数是对基类虚函数的覆盖实现,提高代码的可读性和安全性
使用override关键字可以确保子类中的虚函数命名和基类中的虚函数一致,防止由于命名不一致而导致的意外。
class Base {
public:
virtual void foo();
};
class Derived : public Base {
public:
void foo() override; // 明确标识对基类虚函数foo()的覆盖
};
总之:final关键字用于阻止类或成员函数的继承或重写,而override关键字用于明确标识派生类中对基类虚函数的覆盖实现。这两个关键字都提高了代码的安全性和可读性,并在面向对象编程中起到重要的作用。
2.extern "C"的用法
-
extern “C” 是C++中用于处理C语言和C++语言混合编程的关键字,在C++中,有时候需要与C语言的代码进行链接,或者在C++中调用C语言编写的函数,此时就可以使用extern “C” 来声明C语言的函数接口
-
当使用extern “C” 声明函数时,C++编译器会按照C语言的命名规则来处理该规则,这样可以确保C++和C的函数名在链接时保持一致,避免了由于C++的函数重载等特性导致的函数名修饰不一致的问题
-
假设有以下 C 语言的头文件
functions.h
:
// functions.h
#ifndef FUNCTIONS_H
#define FUNCTIONS_H
int add(int a, int b);
#endif // FUNCTIONS_H
- 对应的 C 语言源文件
functions.c
实现了该头文件中的函数:
// functions.c
#include "functions.h"
int add(int a, int b) {
return a + b;
}
- 现在我们在 C++ 代码中想要调用
add
函数,可以在 C++ 文件中这样声明:
// main.cpp
#include <iostream>
#include "functions.h"
extern "C" {
#include "functions.h" // 使用 extern "C" 包裹 include 指令
}
int main() {
int result = add(5, 10);
std::cout << "Result: " << result << std::endl;
return 0;
}
- 总结:
extern "C"
用于在 C++ 中处理 C 语言的函数接口,保证函数名在链接时的一致性,从而实现 C 和 C++ 的混合编程。这在与 C 语言编写的库或代码进行交互时非常有用。
3.野指针和垂悬指针(悬空指针)
-
野指针是指指针指向的内存地址是无效的,即指针没有被正确初始化,或者指针指向的内存已经被释放,但指针本身还保留着原来的地址。
当我们使用野指针时,由于其指向的内存可能已经被其他程序或系统回收,所以访问该指针指向的内存会导致不可预测的行为,可能会导致程序崩溃或产生错误的结果
野指针的产生通常是由于以下原因:
- 1.未初始化指针:指针变量在定义后没有被正确初始化,它的值是随机的,可能指向任意内存地址
- 指针指向已释放的内存:在使用delete或free释放内存后,没有将指针置为nullptr
int* ptr = new int;
delete ptr; // 释放内存后,ptr 成为野指针,应该将 ptr 置为 nullptr
ptr = nullptr;
- 指针误用:指针被错误的修改,导致它指向了无效的内存地址
int arr[5] = {1, 2, 3, 4, 5};
int* ptr = &arr[0];
ptr += 10; // ptr被错误地修改,指向了不属于arr的内存地址,ptr成为野指针
要避免使用野指针,我们要养成良好的编程习惯,确保指针在定义后都被正确初始化,并在释放指针指向的内存后,立即将指针置为nullptr。同时,不要随意修改指针的值,以防止野指针的产生
-
垂悬指针是指指针指向的内存已经被释放或释放后没有及时置空,导致指针的值仍然是之前指向的地址,但该地址的内容可能已经无效。当我们使用垂悬指针时,由于其指向的内存已经无效,访问该指针指向的内存会导致未定义行为,可能会产生不可预测的结果。
垂悬指针通常是由于以下原因产生:
- 释放后未置空:在使用
delete
或free
释放内存后,没有将指针置为nullptr
- 释放后未置空:在使用
int* ptr = new int;
delete ptr; // 释放内存后,ptr 成为垂悬指针,应该将 ptr 置为 nullptr
ptr = nullptr;
-
- 引用悬挂:当指针指向一个引用,而该引用的生命周期已经结束,指针就会成为垂悬指针。
int a = 10;
int& ref = a;
int* ptr = &ref; // ptr 指向引用 ref,如果在之后 ref 的生命周期结束,ptr 就会成为垂悬指针
-
- 函数返回局部变量地址:在函数内部定义了一个局部变量,并返回其地址,但在函数结束后,该局部变量的内存已经被释放。
要避免使用垂悬指针,我们应该养成良好的编程习惯,在释放指针指向的内存后,立即将指针置为 nullptr
。同时,在返回局部变量的指针时,应该确保返回的是有效的动态内存分配(堆内存)或者静态存储区(全局变量或静态变量)的地址。
4.指针指向的内存被释放是什么意思
释放内存意味着该内存不再属于当前程序,系统可以重新分配给其他程序使用。
5.C和C++的类型安全
-
类型安全是指编程语言或程序设计中的一种属性,确保在程序执行过程中不会发生不合法或未定义的类型操作,类型安全很大程度上可以等价于内存安全,类型安全的代码不会试图访问自己没被授权的内存区域。类型安全保证了以下几点:
- 类型检查:在编译或运行时,编程语言会对类型进行检查,防止不同类型之间的非法操作
- 类型转换的明确性:在进行类型转换时,必须明确指定,避免隐式的不明确转换,从而降低类型错误的概率
- 类型兼容性:确保只有合法的类型转换才能进行,防止发生未定义行为或错误的结果
- 内存安全性:防止越界访问、空指针引用等导致的内存错误,从而提高程序的稳定性和安全性
-
C的类型安全性:
C是一种弱类型语言,类型的转换相对灵活,可以进行隐式类型转化,这使得C中存在潜在的类型错误和未定义行为;C编译器在类型检查方面较弱,无法在编译时完全捕获所有类型错误,可能会导致在运行时出现错误
以下是常见的例子
-
printf格式输出
int main() { printf("int类型输出:%d\n", 10); printf("float类型输出:%f\n", 10); return 0; }
执行结果:
int类型输出:10 float类型输出:0.000000
-
malloc函数的返回值
malloc是C语言中用来进行内存分配的函数,它的返回类型是void* ,我们经常对结果进行类型转换, char* pstr=(char* )malloc(100*sizeof(char)),我们进行了显式类型转换,将void *类型转换为了char * 类型,sizeof的参数类型和显式转换的类型匹配的话,就没什么问题,但是如果不匹配,就很可能带来一些问题,如int * pstr=(int * )malloc(100 * sizeof(char)),而这样的转换C并不会提示错误。
-
-
C++的类型安全性
C++是一种静态类型语言,类型检查在编译时进行,所有类型的转换必须显式指定(除了一些隐式类型转换,如派生类向基类的指针转换)
C++ 提供了更强大的类型系统,包括强制类型转换(
static_cast
、dynamic_cast
、reinterpret_cast
、const_cast
)来明确指定类型转换,以及运算符重载来定义特定类型之间的操作。
6.C++中的重载、重写(覆盖)和隐藏的区别
-
重载
重载是指在同一个作用域内,函数名相同但参数列表不同的情况下,可以定义多个具有相同函数名但参数个数或类型不同的函数。
重载函数可以根据不同的参数来执行相似的操作,提高代码的复用性和可读性
重载函数在编译时通过参数个数、类型和顺序来决定调用哪个函数
class Math {
public:
int add(int a, int b);
double add(double a, double b);
};
-
重写/覆盖(override)
重写是指派生类重新定义基类中的虚函数
重写虚函数允许子类为基类的虚函数提供自己的实现,从而实现多态性
重写发生在类之间的继承关系中,子类函数与父类函数具有相同的函数名、参数列表和返回类型,并且基类函数必须声明为虚函数
class Shape {
public:
virtual void draw() {
std::cout << "Drawing a shape." << std::endl;
}
};
class Circle : public Shape {
public:
void draw() override {
std::cout << "Drawing a circle." << std::endl;
}
};
-
重载和重写的区别:
- 重写是父类和子类之间的垂直关系,重载是不同函数之间的水平关系
- 重写要求参数列表相同,重载则要求参数列表不同,返回值不要求
- 重写关系中,调用方法根据对象类型决定,重载根据调用时实参与形参的对应关系来选择函数体
-
隐藏
隐藏是指在派生类中定义了一个与基类中同名但不具有虚函数特性的函数,这样它就会隐藏基类中的同名函数,包括以下情况:
- 两个函数参数相同,但是基类函数不是虚函数。如果基类函数是虚函数,则就是重写了。
//父类
class A{
public:
void fun(int a){
cout << "A中的fun函数" << endl;
}
};
//子类
class B : public A{
public:
//隐藏父类的fun函数
void fun(int a){
cout << "B中的fun函数" << endl;
}
};
int main(){
B b;
b.fun(2); //调用的是B中的fun函数
b.A::fun(2); //调用A中fun函数
return 0;
}
- 两个函数参数不同,则无论基类函数是否是虚函数,都会被隐藏。和重载的区别在于两个函数不在同一个类中。
//父类
class A {
public:
virtual void fun(int a) {
cout << "A中的fun函数" << endl;
}
};
//子类
class B : public A {
public:
//隐藏父类的fun函数
virtual void fun(char* a) {
cout << "A中的fun函数" << endl;
}
};
int main() {
B b;
b.fun(2); //报错,调用的是B中的fun函数,参数类型不对
b.A::fun(2); //调用A中fun函数
return 0;
}
- 基类指针指向派生类对象时,基类指针可以直接调用到派生类的覆盖函数,也可以通过 :: 调用基类被覆盖的虚函数;而基类指针只能调用基类的被隐藏函数,无法识别派生类中的隐藏函数。