一文了解 kotlin 与 C++的区别

404 阅读32分钟

学习一门新的语言,最好的方法就是从自己熟悉的语言中找不同。这篇文章就从基本语法、逻辑判断和控制、属性、函数、接口、抽象类、类、集合、IO、泛型、异常、多线程、反射角度看 Kotlin 与 C++ 的区别。

基本语法的区别

变量声明

在 C++ 中,变量需要显式声明类型,同时每一个语句后面都需要带上;。代码示例如下:

int val = 10;

函数声明

在 C++ 中函数的声明结构与 kotlin 不同,不需要 fun 关键字,而是和 java 类似。示例如下

返回值 方法名(参数) {
  返回值
}

在 C++ 中对对象的函数调用有两种情况,一种是指针指向的对象,这时使用 -> 调用对应的函数或者获取属性;一种是引用指向的对象,这时使用 . 调用对应的函数或者属性。

逻辑判断和控制

if

if 在 C++ 中用于条件判断,它不可以有返回值。而在 kotlin 中 if 是可以有返回值的。代码示例如下:

#include <iostream>
int main() {
    int num = 10;
    if (num > 5) {
        std::cout << "Number is greater than 5" << std::endl;
    }
    return 0;
}

switch

在 C++ 中,switch 用于多分支选择,表达式必须是整数或者枚举类型。而在 kotlin 中,没有 switch 这个关键字,而是使用 when 来取代它,而且功能更加强大。代码示例如下:

#include <iostream>
int main() {
    int num = 2;
    switch (num) {
        case 1:
            std::cout << "Number is 1" << std::endl;
            break;
        case 2:
            std::cout << "Number is 2" << std::endl;
            break;
        default:
            std::cout << "Number is neither 1 nor 2" << std::endl;
    }
    return 0;
}

for

在 C++ 中,for 关键字用于循环。代码示例如下:

#include <iostream>
#include <vector>
int main() {
    // 传统 for 循环
    for (int i = 0; i < 5; ++i) {
        std::cout << i << std::endl;
    }
    // 范围 for 循环,C++ 11 以后支持
    std::vector<int> vec = {1, 2, 3, 4, 5};
    for (int num : vec) {
        std::cout << num << std::endl;
    }
    return 0;
}

while

在 C++ 中 while 关键字也用于循环。代码示例如下:

#include <iostream>
int main() {
    int i = 0;
    while (i < 5) {
        std::cout << i << std::endl;
        ++i;
    }
    return 0;
}

逻辑运算符

在 C++ 中, 在常用的逻辑运算符有&、&&、|、||、^,它们和在kotlin的中的用法是类似的。

  • &:对两个操作数的对应位进行与运算。
  • &&:逻辑与,只有当两个操作数都为真时结果才为真,具有短路特性。
  • |:对两个操作数的对应位进行或运算。
  • ||:逻辑或,只要有一个操作数为真结果就为真,具有短路特性。
  • ^:对两个操作数的对应位进行异或运算。
// C++ 示例
#include <iostream>
int main() {
    int a = 5; // 二进制: 0101
    int b = 3; // 二进制: 0011
    std::cout << (a & b) << std::endl; // 按位与,结果: 0001 (1)
    std::cout << (a && b) << std::endl; // 逻辑与,结果: 1 (true)
    std::cout << (a | b) << std::endl; // 按位或,结果: 0111 (7)
    std::cout << (a || b) << std::endl; // 逻辑或,结果: 1 (true)
    std::cout << (a ^ b) << std::endl; // 按位异或,结果: 0110 (6)
    return 0;
}

移位操作

在 C++ 中使用 <<(左移)和 >>(右移)运算符进行移位操作。而在 kotlin 中则是使用 shlshr 来实现相同的效果。代码示例如下:

#include <iostream>
int main() {
    int num = 5; // 二进制: 0101
    std::cout << (num << 1) << std::endl; // 左移一位,结果: 1010 (10)
    std::cout << (num >> 1) << std::endl; // 右移一位,结果: 0010 (2)
    return 0;
}

包引入的区别

C++ 中没有 “包”(package)的概念,与之对应的是头文件(header file)的包含机制。使用 #include 预处理指令来包含头文件,从而引入其他文件中定义的类、函数、变量等。代码示例如下:

// 包含标准库的头文件
#include <iostream>
// 包含自定义的头文件
#include "myHeader.h"

int main() {
    std::cout << "Hello, World!" << std::endl;
    // 可以使用 myHeader.h 中定义的内容
    return 0;
}

#include 指令有两种形式:

  • 使用尖括号 <> 包含标准库或系统提供的头文件,编译器会在系统指定的标准库目录中查找这些头文件。
  • 使用双引号 "" 包含自定义的头文件,编译器会先在当前源文件所在的目录中查找,若找不到再到其他指定的目录中查找。

在 C++ 中,头文件包含后,其内容会直接融入当前文件的作用域。如果不同头文件中定义了同名的符号(如函数、类、变量),就会产生命名冲突。为了避免这种情况,通常使用命名空间(namespace)来组织代码。代码示例如下:

// myHeader1.h
namespace MyNamespace1 {
    void func() {
        // 函数实现
    }
}

// myHeader2.h
namespace MyNamespace2 {
    void func() {
        // 函数实现
    }
}

// main.cpp
#include "myHeader1.h"
#include "myHeader2.h"

int main() {
    MyNamespace1::func();
    MyNamespace2::func();
    return 0;
}

属性的区别

访问控制的区别

在C++中只有publicprotectedpublic三种访问修饰符,默认不加修饰符的情况下是private。与 Kotlin 不同的是,所有的修饰符的变量都可以被友元函数访问。访问修饰符的访问范围如下图所示:

privateprotectedpublic
类中的函数
友元函数
子类的函数
类的对象访问

类型的区别

基本类型

与 Kotlin 不同的是,C++的基本数据类型所占用的字节数是根据平台来决定的,要知道确定的大小需要通过sizeof(type)来获取。

在64位操作系统下C++基本数据类型所占用的大小为:

基本数据类型字节
int4
unsigned int4
short2
unsigned short2
long4
unsigned long4
char1
w_char2
unsigned char1
float4
double8
long double8
bool1

数组类型

  • 静态数组和动态数组

    静态数组在内存中位于栈区,是在定义时就已经在栈上分配了固定大小,在运行时这个大小不能改变,在函数执行完以后,系统自动销毁:

    int a[10];
    

    动态数组是malloc或者new出来的,位于内存的堆区,它的大小是在运行时给定,并且可以改变.

    int *a;
    a = new int[10];
    

    注意:动态数组,其创建麻烦,使用完必须由程序员自己通过free或者delete释放,否则严重会引起内存泄露

  • 数组指针

    int *a;
    a = new int[10];
    

    a:是指向第一个数组元素的指针
    *a:获取指针指向的数组的值

指针类型

指针:指针是一块内存,它存放的内容是指向内存的地址

  • 指针的创建

    int i = 10;
    int *p = &i;
    
  • 解引用

    指解析并返回内存地址中保存的值

    int i = 10;
    int *p = &i;
    //解引用  
    //p指向一个内存地址,使用*解出这个地址的值 即为 10
    int pv = *p;
    //修改地址的值,则i值也变成100
    //为解引用的结果赋值也就是为指针所指的内存赋值
    *p = 100;
    
  • 野指针

    未赋值的指针叫野指针

    int val = 10;
    int *p;//野指针
    
  • 悬空指针

    悬空指针:指针最初指向的内存已经被释放了的一种指针

  • 指针占用内存的大小

    在32位中指针占用4字节,在64位中为8字节

    //32位:
    sizeof(p) == 4;
    //64位:
    sizeof(p) == 8;
    
  • 指针运算

    int val = 10;
    int *p = &val;
    //指针可以进行 + - ++ -- 操作
    p++;//表示指针指向下一个内存位置,这里移动 sizeof(int)字节
    
  • 为什么需要指针指向char、int等数据类型

    因为这些数据类型表示的是从指针存储的地址中一次能够读写的数据字节数。例如

    char *d;
    short *e;
    long *f;
    

    假设d、e、f的值都是100。在这种情况下,使用d时就能够从编号100的地址中读写1个字节的数据,使用e时就是2个字节(100-101地址)的数据,使用f时就是4个字节(100-104地址)的数据。

  • 为什么创建指针时需要初始化

    //错误示例
    int *p;//指针p的值可能为任何值,有可能性和其他地址冲突
    *p = 100;//如果产生冲突,赋值会修改冲突地址的值
    
    //正确示例
    int *p = &0;//初始化
    *p = 100;
    

    创建指针未初始化时,该指针的值可能为任何值,就有可能会和其他的地址的值产生冲突;如果产生冲突,赋值会修改冲突地址的值。

  • 数组指针和指针数组

//数组指针,指向数组的指针
int array[2][3] = {{1,2,3},{4,5,6}};
int (*p)[3] = array;
//获取 5 
int val = *(*(p+1)+1);

//指针数组,一个数组里面的值的指针
int val = 1;
int *array1[3] = {&val,&val,&val};

数组名是数组首元素的地址;c语言中,二维数组的占用的内存(例如int p[2][2])和一维数组int p[4]相同,二维数组的内存也是连续的。

  • const char *, char const *, char * const,char const * const的区别
char[] temp = "hello";
//从右到左来看
const char *p1 = temp;//指向不可变数组,即不能通过 p1 来修改数组的值
char const *p2 = temp;//和 p2 相同
char * const p3 = temp;//不可变指针,指针不能指向其他的地址,可以通过 p3 来修改数组的值
const char * const p4 = temp;//不可变指针指向不可变数组,指针不能指向其他的地址,不能通过 p3 来修改值
char const * const p5 = temp;//和 p4 相同
  • 多级指针
int i = 1;
//存放i的地址
int *p = &i;
//存放 p 指针的地址,注意 int *p1 = &p 是不支持的
int **p1 = &p;

cout << "&i = " << &i << endl;
cout << "p = " << p << endl;
cout << "*p = " << *p << endl;
cout << "&p = " << &p << endl;
cout << "p1 = " << p1 << endl;
cout << "*p1 = " << *p1 << endl;
cout << "&p1 = " << &p1 << endl;
cout << "**p1 =" << **p1 << endl;

上面代码的效果如下图所示:

image.png

经常使用的指针一般为一级、二级指针;很少使用三级及以上的指针

引用类型

引用变量是一个别名,也就是说,它是某个已存在变量的另一个名字。一旦把引用初始化为某个变量,就可以使用该引用名称或变量名称来指向变量。

  • 引用的使用
int val = 10;
//声明引用
int& p = val;
// 引用使用
std::cout << "p 的内容: " << p << std::endl;
std::cout << "val 的内存地址: " << &val << std::endl;
std::cout << "引用 p 的内存地址: " << &p << std::endl; 
    
//把引用作为参数
void func(int& v){
    
}
func(p);
//把引用作为返回值
int& func_1(){
  //不能返回局部变量,但是可以返回static修饰的变量
  static int p = 1;
  return p;
}

引用 和 指针的区别

  • 不存在空引用,引用必须连接到一块合法的内存。但可以存在空指针。
  • 一旦引用被初始化为一个对象,就不能被指向到另一个对象。指针可以在任何时候指向到另一个对象。
  • 引用必须在创建时被初始化。指针可以在任何时间被初始化。

小结

  • 指针变量:通过 & 获取自身地址,通过 * 解引用获取值,通过 -> 调用成员。
  • 引用变量:本质是别名,操作方式与原变量一致,&ref 实际是原变量的地址。

类型判断和转换

在 kotlin 中类型判断使用的是关键字 is ,而类型转换则是关键字 as。而在 C++ 中使用类型判断和转换的功能则更复杂些。

类型判断

在 C++ 中使用 typeid 运算符用于在运行时获取类型信息。typeid 可以作用于对象(带有多态行为的指针或引用)或类型(无需实例化对象)。代码示例如下:

#include <iostream>
#include <typeinfo>

class Base { virtual void func() {} };
class Derived : public Base {};

int main() {
    int num = 10;
    std::cout << "num 的类型: " << typeid(num).name() << std::endl;

    Base* basePtr = new Derived();
    if (typeid(*basePtr) == typeid(Derived)) {
        std::cout << "basePtr 指向 Derived 类型对象" << std::endl;
    }
    delete basePtr;
    return 0;
}
类型转换

在C语言中可以通过(需要转换的类型)原类型来实现强制转换;在C++增加了const_caststatic_castdynamic_castreinterpret_cast四种新式转换。

  • const_cast

用来修改类型的const或volatile属性

const char *a;
char *b = const_cast<char*>(a);
	
char *a;
const char *b = const_cast<const char*>(a);
  • static_cast

应用场景:

  1. 基础类型之间互转。如:float转成intint转成unsigned int
  2. 指针与void之间互转。如:float*转成void*Bean*转成void*函数指针转成void*
  3. 子类指针/引用父类指针/引用 进行转换。
class Parent {
public:
	void test() {
		cout << "p" << endl;
	}
};
class Child :public Parent{
public:
	 void test() {
		cout << "c" << endl;
	}
};
Parent  *p = new Parent();
Child  *c = static_cast<Child*>(p);
//输出c
//Parent test加上 virtual时才输出 p
c->test();
  • dynamic_cast

主要 将基类指针、引用 安全地转为派生类.在运行期对可疑的转型操作进行安全检查,仅对多态有效

//基类至少有一个虚函数
//对指针转换失败的得到NULL,对引用失败  抛出bad_cast异常 
Parent  *p = new Parent();
Child  *c = dynamic_cast<Child*>(p);
if (!c) {
	cout << "转换失败" << endl;
}

Parent  *p = new Child;
Child  *c = dynamic_cast<Child*>(p);
if (c) {
	cout << "转换成功" << endl;
}
  • reinterpret_cast

对指针、引用进行原始转换

float i = 10;

//&i float指针,指向一个地址,转换为int类型,j就是这个地址
int j = reinterpret_cast<int>(&i);
cout  << hex << &i << endl;
cout  << hex  << j << endl;

cout<<hex<<i<<endl; //输出十六进制数
cout<<oct<<i<<endl; //输出八进制数
cout<<dec<<i<<endl; //输出十进制数

常量的区别

const 和 constexpr

在 kotlin 中,使用 val 表示只读变量,而 const 来表示常量。而在 C++ 中,使用 constconstexpr这两个关键字来定义常量,但在使用上存在一些差异。

  • const

const 关键字用于定义一个值不可变的常量。它可以用于变量、指针、引用以及类的成员函数。当const修饰一个变量时,这个变量的值在程序执行过程中不能被修改。例如,定义一个const整数:

//在这个例子中,max_value一旦被赋值,其值就不能再改变。
const int max_value = 100;
  • constexpr

constexpr关键字是在C++11中引入的,用于定义在编译时就必须确定值的表达式。这意味着constexpr变量的值必须在编译时就能够确定,而且constexpr函数的返回值也必须是一个常量表达式。例如,定义一个constexpr函数来计算平方:

constexpr int square(int x) {
    return x * x;
}

宏定义

除了使用上面的关键字定义常量外,C++还可以使用宏定义来定义常量。宏定义是预处理器的功能,在编译之前,预处理器会对源代码中的宏进行简单的文本替换。宏没有类型信息,只是单纯的文本替换机制。代码示例如下:

//宏一般使用大写区分
//宏变量
//在代码中使用 A 就会被替换为1
#define A 1

变量的区别

在C++,有局部变量成员变量静态变量静态局部变量全局变量静态全局变量.

  • 局部变量

    在函数或一个代码块内部声明的变量,称为局部变量。

    void func(){
      int a = 0;//局部变量
      ...
    }
    
  • 静态局部变量

    静态局部变量在方法执行完后不会被消除,可以延长局部变量的生命周期,并且只初始化一次

    void func(){
      static int data = 0;
      ...
    }
    
  • 成员变量

    在类中声明的变量称为成员变量。如下变量i、j、k是成员变量。

    class Student {
      int i;//默认修饰符是private
      public:
        Student(int i,int j,int k):i(i),j(j),k(k){};	//构造方法 
        ~Student(){};	//析构方法 
      private:
        int j;
      protected:
        int k;
    };
    
  • 静态变量

    //Instance.h
    class Instance {
    public:
      ...
    private:
        static Instance *instance;
    };
    #endif 
    
    //Instance.cpp
    #include "Instance.h"
    Instance* Instance::instance = 0;//必须初始化
    ...
    

    在C++类中声明静态变量,就必须在cpp文件中对它进行初始化,否则会报错

  • 全局变量

    在所有函数外部声明的变量,称为全局变量。全局变量不是在类中声明的,在类中声明的是成员变量。

    int index = 0;
    ...
    

    全局变量是C语言特性,可以在整个文件中使用。使用extern关键字,还能在其他文件使用。

  • 静态全局变量

    全局变量声明static关键字,就是静态全局变量。

    static int index = 0;
    ...
    

    与全局变量的区别就是不能在其他文件中使用。

函数的区别

构造函数

类的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。 构造函数的名称与类的名称是完全相同的,构造函数可用于为某些成员变量设置初始值。

class Student {
  public:
    Student();//无参构造函数
    Student(int i,int j,int k);//带参数的构造函数
    Student(int i,int j,int k):i(i),j(j),k(k);//使用初始化列表来初始化,等同于下面的
    Student(int i,int j,int k){
      this->i = i;
      this->j = j;
      this->k = k;
    }
  ...
};

析构函数

类的析构函数是类的一种特殊的成员函数,它会在每次删除所创建的对象时执行。析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数。析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源。

class Student {
  public:
    ~Student(){};	//析构方法 
};

声明函数

  • 非成员函数

    //void : 返回类型
    //func :方法名
    // data : 方法参数
    void func(int data){
      //函数体
    }
    
  • 成员函数

    //Student.h
    class Student{
      public:
        ...
        int getGrade(int id);
    }
    //Student.cpp
    #include "Student.h"
    int Student::getGrade(int id){
      ...
    }
    

成员函数和非成员函数的区别:成员函数是在类中定义的函数,而非成员函数就是普通函数,即不在类中定义的函数,其中非成员函数比较典型的是友元函数

  • 静态函数

    //Instance.h
    class Instance {
    public:
        static Instance* getInstance();//声明静态方法
    private:
        static Instance *instance;
    };
    #endif 
    
    //Instance.cpp
    #include "Instance.h"
    Instance* Instance::instance = 0;
    Instance* Instance::getInstance() {//实现静态方法
        //C++11以后,编译器保证内部静态变量的线程安全性
        if (!instance) {
            instance = new Instance;
        }
        return instance;
    }
    

方法参数

  • 函数参数的类型

    1. 传值调用:把参数的值复制给函数的形式参数。修改形参不会影响实参

    2. 引用调用:形参为指向实参地址的指针或者指向原变量的引用,可以通过指针或者引用修改实参。

  • 不定参数

和Java类似,c++的方法的参数也有不定参数。代码如下:

//先引入头文件<stdarg.h>
#include <stdarg.h>
//创建函数,注意第一个参数是必须的,不限定类型;
//第二个参数表示不定参数
int add(int num,int ...)
{
	va_list valist;
	int sum = 0;
	// 初始化  valist指向第一个可变参数 (...)
	va_start(valist, num);
	for (size_t i = 0; i < num; i++)
	{
		//访问所有赋给 valist 的参数,
		int j = va_arg(valist, int);
		printf("%d\n", j);
		sum += j;
	}
	//清理为 valist 内存
	va_end(valist);
	return sum;
}

方法指针(函数指针)

函数指针是指向函数的指针变量。定义如下:

void println(char *buffer) {
	printf("%s\n", buffer);
}
//接受一个函数作为参数
//void(*p)(char*) void表示返回值 p表示这个函数 char*表示函数参数
void say(void(*p)(char*), char *buffer) {
	p(buffer);
}
//使用
void(*p)(char*) = println;
//使用函数指针
p("hello");
//传递参数
say(println, "hello");

函数指针的应用,比如回调函数。

//typedef 创建别名 由编译器执行解释
//typedef unsigned char u_char;
typedef void(*Fun)(char *);
Fun fun = println;
fun("hello");
say(fun, "hello");

//类似java的回调函数
typedef void(*Callback)(int);

void test(Callback callback) {
	callback("成功");
	callback("失败");
}
void callback(char *msg) {
	printf("%s\n", msg);
}

test(callback);

指针函数:指针函数就是指返回指针的函数

常量函数

函数后写上const,表示不会也不允许修改类中的成员。

class Student {
	int i;
public:
	Student() {}
	~Student() {}
	// 常量函数
	void  setName(char* _name) const  {
		//错误 不能修改name 去掉const之后可以
		name = _name;
	}
private:
	int j;
	char *name;
protected:
	int k;
};

拷贝函数

拷贝构造函数是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。如果在类中没有定义拷贝构造函数,编译器会自行定义一个。

class Line
{
   public:          
      Line( const Line &obj);      // 默认拷贝构造函数
     ...                
};
  1. 浅拷贝

    当类中没有指针的时候,可以使用浅拷贝。浅拷贝不需要声明拷贝函数,直接使用默认的就行。

  2. 深拷贝

    当类中有指针类型时,需要定义拷贝函数,示例:

    class Line
    {
     public:
        Line( const Line &obj){     // 拷贝构造函数
           ptr = new int;
           *ptr = *obj.ptr; // 拷贝值
        }
        ... 
     private:
        int *ptr;
    };
    
  3. 拷贝构造函数调用时机

//初始化时
Line line;
Line copyLine(&line);
//作为参数时
void draw(Line line);
//作为返回值时
Line get();

重载函数

C++ 允许在同一作用域中的某个函数运算符指定多个定义,分为函数重载运算符重载

  • 函数重载

    void print(int i) {
            cout << "整数为: " << i << endl;
    }
    
    void print(double  f) {
            cout << "浮点数为: " << f << endl;
    }
    
  • 操作符重载

    C++允许重定义或重载大部分 C++ 内置的运算符,其中重载的函数名是由关键字 operator 和其后要重载的运算符符号构成的。 示例如下:

    示例1,重载+运算符:

    class Test2 {
    public:
            int i;
    };
    //定义非成员函数进行 + 重载
    Test2 operator+(const Test2& t21, const Test2& t22) {
            Test2 t;
            t.i = t21.i + t22.i;
            return t;
    }
    
    Test2 t21;
    Test2 t22;
    t21.i = 100;
    t22.i = 200;
    Test2 t23 = t21 + t22;
    cout << t23.i << endl;
    

    示例2,重载newdelete运算符:

    void *operator new (size_t size)
    {
            cout << "新的new:" << size << endl;
            return malloc(size);
    }
    
    void operator delete(void *p)
    {
            //释放由p指向的存储空间
            cout << "新的delete" << endl;
            free(p);
    }
    ... ...
    

允许重载的运算符如下:

类型运算符
关系运算符==(等于),!= (不等于),< (小于),> (大于>,<=(小于等于),>=(大于等于)
逻辑运算符||(逻辑或),&&(逻辑与),!(逻辑非)
单目运算符+ (正),-(负),*(指针),&(取地址)
自增自减运算符++(自增),--(自减)
位运算符| (按位或),& (按位与),~(按位取反),^(按位异或),,<< (左移),>>(右移)
赋值运算符=, +=, -=, *=, /= , % = , &=, |=, ^=, <<=, >>=
空间申请与释放new, delete, new[ ] , delete[]
其他运算符()(函数调用),->(成员访问),,(逗号),

宏函数

//宏函数
#defind test(i) i > 10 ? 1: 0

//其他技巧
// # 连接符 连接两个符号组成新符号
#define DN_INT(arg) int dn_ ## arg
DN_INT(i) = 10;
dn_i = 100;

// \ 换行符
#define PRINT_I(arg) if(arg) { \
 printf("%d\n",arg); \
 }
PRINT_I(dn_i);

//可变宏
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,"NDK", __VA_ARGS__);

//陷阱
#define MULTI(x,y)  x*y
//获得 4
printf("%d\n", MULTI(2, 2));
//获得 1+1*2  = 3
printf("%d\n", MULTI(1+1, 2));

优点:文本替换,每个使用到的地方都会替换为宏定义。不会造成函数调用的开销(开辟栈空间,记录返回地址,将形参压栈,从函数返回还要释放堆栈。) 缺点:生成的目标文件大,不会执行代码检查

  • 宏函数与内联函数的区别

    内联函数有类型检查同时也可以debug,但是内联函数不能包含复杂的控制语句,while、switch,并且内联函数本身不能直接调用自身。如果内联函数的函数体过大,编译器会自动的把这个内联函数变成普通函数。

  • typedef 和 #define 的区别:

    #define 用于为各种数据类型定义别名,与 typedef 类似,但是它们有以下几点不同:

    1. typedef 仅限于为类型定义符号名称,#define 不仅可以为类型定义别名,也能为数值定义别名,比如您可以定义 1 为 ONE。

    2. typedef 是由编译器执行解释的,#define 语句是由预编译器进行处理的。

友元类和友元函数

类的友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。 友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类,在这种情况下,整个类及其所有成员都是友元。

友元类如下:

class Student {
	int i;
public:
	Student() {}
	~Student() {}
	void  setName(char* _name)  {
		name = _name;
	}
	friend void printName(Student *student);
    //友元类
	friend class Teacher;
private:
	int j;
	char *name;
protected:
	int k;
};

class Teacher {
public:
	void call(Student *student) {
        //能够使用student中私有的name属性
		cout << "call:" << student->name << endl;
	}
};

有元函数如下:

class Student {
	int i;
public:
	Student() {}
	~Student() {}
	void  setName(char* _name)  {
		name = _name;
	}
	friend void printName(Student *student);
private:
	int j;
	char *name;
protected:
	int k;
};

void printName(Student *student) {
    //能够使用student中私有的name属性
	cout << student->name << endl;
}

Student *student = new Student;
student->setName("Lance");
printName(student);

在 C++ 里,友元类和友元函数是特殊的语言特性,突破了类的封装性,允许特定的类或函数访问另一个类的私有和保护成员。

抽象类的区别

在 C++ 中没有接口的概念,只有抽象类的概念。在 C++ 中,存在 virtual 定义的纯虚函数的类就叫抽象类。代码示例如下:

#include <iostream>

// 定义抽象类
class Shape {
public:
    // 纯虚函数
    virtual double area() const = 0;
    virtual void printInfo() {
        std::cout << "This is a shape." << std::endl;
    }
    virtual ~Shape() {}
};

类的区别

继承的区别

class A:[private/protected/public] B表示A继承B。A是基类,B称为子类或者派生类。

方式说明
public基类的public、protected成员也是派生类相应的成员,基类的private成员不能直接被派生类访问,但是可以通过调用基类的公有和保护成员来访问。
protected基类的公有和保护成员将成为派生类的保护成员
private基类的公有和保护成员将成为派生类的私有成员,默认为private继承

单继承

class Parent {
public:
    Parent(string name){
      this->name = name;
    }
	void test() {
		cout << "parent" << endl;
	}
};

class Child : Parent {
public:
    Child(string name):Parent(name){
      this->name = name;
    }
	void test() {
         // 调用父类 方法
		Parent::test();
        //使用 __super::test();调用父类方法
        __super::test();
		cout << "child" << endl;
	}
};

多继承

class <派生类名>:<继承方式1><基类名1>,<继承方式2><基类名2>,…实现多继承;示例如下:

class Father {
public:
	Father(string name) {
		this->name = name;
		this->age = 0;
	}
	Father(string name, int age) {
		this->age = age;
		this->name = name;
	}
	virtual void work() {
		cout << name<<"在工作赚钱" << endl;
	}

	virtual void sleep() {
		cout << "爸爸在睡觉" << endl;
	}
private:
	string name;
	int age;
};

class Mother {
public:
	Mother(string name) {
		this->name = name;
		this->age = 0;
	}
	Mother(string name, int age) {
		this->age = age;
		this->name = name;
	}
	virtual void cook() {
		cout << name<<"在做饭" << endl;
	}

	virtual void sleep() {
		cout << "妈妈在睡觉" << endl;
	}
private:
	string name;
	int age;
};

class Child : public Father, public Mother{
public:
	Child(string name) :Father(name), Mother(name) {
		this->name = name;
		this->age = 0;
	}

	Child(string name,int age) :Father(name,age), Mother(name,age) {
		this->name = name;
		this->age = age;
	}

	void printInfo() {
		cout << name << " " << age << endl;
	}

	virtual void sleep() {
		__super::Father::sleep();
		cout << name << "在睡觉" << endl;
	}
private:
	string name;
	int age;
};

int main() {
	Child child("小明");
	child.cook();
	child.work();
	child.printInfo();
	child.sleep();
}

多态的区别

静态多态

静态多态(静态联编)是指在编译期间就可以确定函数的调用地址,通过函数重载模版(泛型编程) 实现

class Parent {
public:
	 void test() {
		cout << "parent" << endl;
	}
};

class Child :public Parent {
public:
	void test() {
		cout << "child" << endl;
	}
};

Parent *c = new Child();
// 编译期间 确定c 为 parent 调用parent的test方法
c->test();

//修改Parent为virtual 虚函数 动态链接,告诉编译器不要静态链接到该函数
virtual void test() {
	cout << "parent" << endl;
}
//动态多态 调用Child的test方法
c->test();

动态多态

动态多态(动态联编)是指函数调用的地址不能在编译器期间确定,必须需要在运行时才确定 ,通过继承+虚函数 实现。构造函数任何时候都不可以声明为虚函数,析构函数一般都是虚函数,释放先执行子类再执行父类

  • 纯虚函数

类似于Java的抽象方法。

class Parent {
public:
    //纯虚函数 继承自这个类需要实现 抽象类型
	virtual void test() = 0;
};

class Child :public Parent {
public:
	void test(){}
};

集合的区别

常用的数据结构包括:数组array, 链表list, 树tree, 栈stack, 队列queue, 散列表hash table, 集合set、映射表map 等等。容器便是容纳这些数据结构的。这些数据结构分为序列式与关联式两种,容器也分为序列式容器和关联式容器。

序列式容器/顺序容器

元素排列次序与元素无关,由元素添加到容器的顺序决定

容器说明
vector支持快速随机访问
list支持快速插入、删除
deque双端队列 允许两端都可以进行入队和出队操作的队列
stack后进先出LIFO(Last In First Out)堆栈
queue先进先出FIFO(First Input First Output)队列
priority_queue有优先级管理的queue

向量(vector) 、列表 (list)、双端队列(deque)

向量(vector) :连续存储的元素
列表 (list):由节点组成的双向链表,每个结点包含着一个元素
双端队列(deque):连续存储的指向不同元素的指针所组成的数组

以上三种容器操作基本一样

基本操作:

#include<iostream>
#include <vector>
using namespace std;
int main() {
    //创建vector对象
    vector<int> vec_1;
    vector<int> vec_2(1);    //1个元素
    vector<int> vec_3(6,1);    //6个值为 1 的元素
    vector<int> vec_4(vec_3);    //使用容器初始化
    //增加
	vec.push_back(2);//往末尾插入元素
	vec.insert(vec.begin()+0, 0);//往第一个位置插入数据
	vec.insert(vec.begin() + 2, 6);//往第三个位置插入数据
	//删除
	vec.erase(vec.begin());//删除第一个元素
	vec.erase(vec.begin(), vec.begin() + 2);//删除指定范围的元素
	vec.clear();//清空容器
	//改变指定元素
	vec[0] = 2;
	//查找指定位置的元素
	cout << vec[2] << endl;
    //首尾元素
    vec_3.front()
    vec_3.back()
	//遍历1
	for (int i = 0; i < vec.size(); i++) {
		cout << vec[i];
	}
	//遍历2
	vector<int>::iterator it = vec.begin();
	for (; it < vec.end(); it++) {
		cout << *it;
	}
    //迭代器
    //获得指向首元素的迭代器  模板类,不是指针,当做指针来使用
    vector<int>::iterator it = vec.begin();
    //遍历元素
    for (; it < vec.end(); it++)
    {
        cout << *it << endl;
    }
    //begin和end   分别获得 指向容器第一个元素和最后一个元素下一个位置的迭代器
    //rbegin和rend 分别获得 指向容器最后一个元素和第一个元素前一个位置的迭代器
    //注意循环中操作元素对迭代器的影响
    vector<int>::iterator it = vec.begin();
    for (; it < vec.end(); )
    {
        //删除值为2的元素 
        if (*it == 2) {
            vec.erase(it);
        }
        else {
            it++;
        }
    }
}

栈(stack)

后进先出的值的排列

stack<int> s;
//入栈
s.push(1);
s.push(2);
//弹栈
s.pop();
//获取栈顶
cout << s.top() << endl;

队列(queue)

先进先出的值的排列

queue<int> q;
q.push(1);
q.push(2);
//移除最后一个
q.pop();
//获得第一个
q.front();
//最后一个元素
cout << q.back() << endl;

优先队列(priority_queue )

元素的次序是由所存储的数据的某个值排列的一种队列

//最大的在队首
priority_queue<int>;
//在vector之上实现的
priority_queue<int, vector<int>, less<int> >; 
//vector 承载底层数据结构堆的容器
//less 表示数字大的优先级高,而 greater 表示数字小的优先级高
//less  	 让优先队列总是把最大的元素放在队首
//greater    让优先队列总是把最小的元素放在队首

//less和greater都是一个模板结构体 也可以自定义

class Student {
public:
	int grade;
	Student(int grade):grade(grade) {
	}
};
struct cmp {
	bool operator ()(Student* s1, Student* s2) {
        // > 从小到大
        // < 从大到小 
		return s1->grade > s2->grade;
	}
	bool operator ()(Student s1, Student s2) {
		return s1.grade > s2.grade;
	}
};
priority_queue<Student*, vector<Student*>, cmp > q1;
q1.push(new Student(2));
q1.push(new Student(1));
q1.push(new Student(3));
cout << q1.top()->grade << endl;

关联式容器

关联容器中的元素是按关键字来保存和访问的 支持高效的关键字查找与访问

集合(set)

由节点组成的红黑树,每个节点都包含着一个元素,元素不可重复

set<string> a;  
set<string> a1={"fengxin","666"};
a.insert("fengxin");  // 插入一个元素
a.erase("123");	//删除

键值对(map)

由{键,值}对组成的集合

map<int, string> m;
map<int, string> m1 = { { 1,"Lance" },{ 2,"David" } };
//插入元素
m1.insert({ 3,"Jett" });
//pair=键值对
pair<int, string> p(4, "dongnao");
m1.insert(p);
//insetrt 返回 map<int, string>::iterator : bool 键值对
//如果 插入已经存在的 key,则会插入失败   
//multimap:允许重复key
//使用m1[3] = "xx" 能够覆盖


//通过【key】操作元素
m1[5] = "yihan";
cout << m1[5].c_str() << endl; 
//通过key查找元素
map<int, string>::iterator it = m1.find(3);
cout << (*it).second.c_str()<< endl;
// 删除 
m1.erase(5);
//遍历
for (it = m1.begin(); it != m1.end(); it++)
{
	pair<int, string> item = *it;
	cout << item.first << ":" << item.second.c_str() << endl;
}

//其他map================================

unordered_map c++11取代hash_map(哈希表实现,无序) 需要无序容器,高频快速查找删除,数据量较大用unordered_map; 需要有序容器,查找删除频率稳定,在意内存时用map。

IO的区别

C语言的I/O操作

导入头文件stdio.h,使用函数原型:FILE * fopen(const char * path, const char * mode); 就可以获取文件对象,对文件进行操作。

mode模式如下:

模式描述
r打开一个已有的文本文件,允许读取文件。
w打开一个文本文件,允许写入文件。如果文件不存在,则会创建一个新文件。在这里,您的程序会从文件的开头写入内容。如果文件存在,则该会被截断为零长度,重新写入。
a打开一个文本文件,以追加模式写入文件。如果文件不存在,则会创建一个新文件。在这里,您的程序会在已有的文件内容中追加内容。
r+打开一个文本文件,允许读写文件。
w+打开一个文本文件,允许读写文件。如果文件已存在,则文件会被截断为零长度,如果文件不存在,则会创建一个新文件。
a+打开一个文本文件,允许读写文件。如果文件不存在,则会创建一个新文件。读取会从文件的开头开始,写入则只能是追加模式。

示例1:往文件写入内容

FILE *f = fopen("xxxx\\t.txt","w");
//写入单个字符
fputc('a', f);
//写入字符串
char *txt = "123456";
fputs(txt, f);
//格式化并写入文件
fprintf(f,"%s",txt);
fclose(f);

示例2:读取文件内容

FILE *f = fopen("xxxx\\t.txt","w");
//读取一个字符
char c = fgetc(f); 
//读取 遇到第一个空格字符停止
char buff[255];
fscanf(f, "%s", buff);
printf("1: %s\n", buff);
//最大读取 255-1 个字符
fgets(buff, 255, f);
printf("2: %s\n", buff);
fclose(f);

示例3:读取和写入二进制数据

#include <stdio.h>
#include <string.h>
 
int main()
{
   FILE *fp;
   char c[] = "This is runoob";
   char buffer[20];
 
   /* 打开文件用于读写 */
   fp = fopen("file.txt", "w+");
 
   /* 写入数据到文件 */
   fwrite(c, strlen(c) + 1, 1, fp);
 
   /* 查找文件的开头 */
   fseek(fp, 0, SEEK_SET);
 
   /* 读取并显示数据 */
   fread(buffer, strlen(c)+1, 1, fp);
   printf("%s\n", buffer);
   fclose(fp);
   
   return(0);
}

C++的I/O操作

导入<iostream><fstream>

数据类型描述
ofstream输出文件流,创建文件并向文件写入信息。
ifstream输入文件流,从文件读取信息。
fstream文件流,且同时具有 ofstream 和 ifstream 两种功能。
char data[100];
// 以写模式打开文件
ofstream outfile;
outfile.open("XXX\\f.txt");
cout << "输入你的名字: ";
//cin 接收终端的输入
cin >> data;
// 向文件写入用户输入的数据
outfile << data << endl;
// 关闭打开的文件
outfile.close();

// 以读模式打开文件
ifstream infile;
infile.open("XXX\\f.txt");

cout << "读取文件" << endl;
infile >> data;
cout << data << endl;

// 关闭
infile.close();

泛型(模板)的区别

函数模板

函数模板能够用来创建一个通用的函数。以支持多种不同的形參。避免重载函数的函数体反复设计。

template <typename T> //也可以使用 template <class T>
T max(T a,T b)
{
	// 函数的主体
	return  a > b ? a : b;
}
//代替了
int max(int a,int b)
int max(float a,float b)

类模板(泛型类)

为类定义一种模式。使得类中的某些数据成员、默写成员函数的參数、某些成员函数的返回值,能够取随意类型.常见的 容器比如 向量 vector 或 vector 就是模板类。

//定义两个不同的模板
template<class E,class T>
class Queue {
public:
    T add(E e,T t){
        return e+t;
    }
};

Queue<int,float> q;
q.add(1,1.1f) = 2.1f

非类型模版参数

非类型模板参数可以是整形、枚举或者外部链接指针

template<typename T, int MAXSIZE>
class Stack{
Private:
       T elems[MAXSIZE];
       ...
};

int main()
{
       //使用
       Stack<int, 20> stack;
};

模板注意事项

  • 使用typedef时产生的问题
  1. 返回class中通过typedef定义的变量时,需要添加typename
//Queue.h
#pragma once
template <typename T>
class Queue {
public:
	T data;
	typedef unsigned int queue_size;
	queue_size size();//返回使用typedef定义的queue_size
};
//Queue.cpp
template<class T>//声明方法和变量时必须添加
T data;
template <class T> 
typename Queue<T>::queue_size Queue<T>::size() {//需要加上typename,否则无法编译
	return 0;
}
  • 导入问题

当模板声明和模板定义分开时,需要同时导入.h.cpp文件。

//Queue.h
#pragma once
template <typename T>
class Queue {
public:
	T data;
	typedef unsigned int queue_size;
	queue_size size();
};
//Queue.cpp
template<class T>
T data;
template <class T> 
typename Queue<T>::queue_size Queue<T>::size() {
	return 0;
}
//Main.cpp
#include"Queue.h"//需要同时导入Queue.h Queue.cpp
#include"Queue.cpp"
#include<iostream>
#include <vector>
#include<stdarg.h>
int main() {
	...
}

异常的区别

void test1()
{
	throw "测试!";
}

void test2()
{
	throw exception("测试");
}

try {
	test1();
}
catch (const char *m) {
	cout << m << endl;
}
try {
	test2();
}
catch (exception  &e) {
	cout << e.what() << endl;
}

//自定义
class MyException : public exception
{
public:
   virtual char const* what() const
    {
        return "myexception";
    }
};

多线程的区别

【C++】多线程编程图文详解

反射的区别

C++ 标准库本身对反射的支持非常有限。直到 C++23 之前,并没有完整的内置反射机制。不过,在某些特定场景下可以通过一些技巧来模拟反射的部分功能,例如使用运行时类型信息(RTTI),它能在运行时识别对象的类型。RTTI 主要通过 typeid 和 dynamic_cast 来实现。代码示例如下:

#include <iostream>
#include <typeinfo>

class Base { virtual void func() {} };
class Derived : public Base {};

int main() {
    Base* basePtr = new Derived();
    // 使用 typeid 获取类型信息
    if (typeid(*basePtr) == typeid(Derived)) {
        std::cout << "basePtr 指向 Derived 类型对象" << std::endl;
    }
    // 使用 dynamic_cast 进行安全的向下转换
    Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);
    if (derivedPtr) {
        std::cout << "转换成功" << std::endl;
    }
    delete basePtr;
    return 0;
}    

参考