c++ 函数 类

一、函数定义

在 C++ 中,函数是组织代码逻辑的基本单元,用于实现模块化、复用、结构清晰的程序设计。


1、函数的基本结构

cpp 复制代码
返回类型 函数名(参数列表) {
    // 函数体
    return 值; // 可选,视返回类型而定
}

声明(Declaration):

告诉编译器函数存在,通常放在头文件中:

cpp 复制代码
int add(int a, int b);  // 函数声明

定义(Definition):

提供函数实现,通常放在 .cpp 文件中:

cpp 复制代码
int add(int a, int b) {
    return a + b;
}

2、函数重载(Overload)

同一个函数名可以定义多个参数不同的函数:

cpp 复制代码
void print(int x);
void print(double x);
void print(std::string s);

注意:参数数量或类型不同才能构成重载,返回类型不同不能单独构成重载。


3、默认参数值

cpp 复制代码
void greet(std::string name = "Guest") {
    std::cout << "Hello, " << name << "!\n";
}

greet();         // 输出 Hello, Guest!
greet("Alice");  // 输出 Hello, Alice!

4、内联函数(inline

建议编译器将函数代码插入调用处,适用于短小频繁调用的函数。

cpp 复制代码
inline int square(int x) {
    return x * x;
}

5、虚函数与纯虚函数

在 C++ 中,虚函数(virtual纯虚函数(= 0 是实现 多态性 的关键机制,但它们在语法、用途、作用上有所不同。

特性 虚函数(Virtual Function) 纯虚函数(Pure Virtual Function)
定义方式 virtual void foo(); virtual void foo() = 0;
是否有实现 ✅ 可以有实现(也可以没有) ❌ 必须在子类中实现(抽象接口)
是否必须重写 ❌ 子类可选是否重写 ✅ 子类必须重写(除非子类也是抽象类)
所在类 可以在任何类中 必须出现在抽象类中(即包含纯虚函数的类)
创建对象 ✅ 可以实例化含虚函数的类 ❌ 抽象类不可被实例化
用途 提供多态行为的默认实现 强制子类实现,作为接口规范

🔷 1. 虚函数示例(可重写)

cpp 复制代码
class Animal {
public:
    virtual void speak() {
        std::cout << "Animal speaks\n";
    }
};

class Dog : public Animal {
public:
    void speak() override {
        std::cout << "Dog barks\n";
    }
};

Animal* p = new Dog();
p->speak();  // 输出:Dog barks(多态)

🔸 如果 Dog 不重写 speak(),则会使用 Animal 的默认实现。


🔷 2. 纯虚函数示例(强制重写)

cpp 复制代码
class Shape {
public:
    virtual void draw() = 0;  // 纯虚函数
};

class Circle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing Circle\n";
    }
};

// Shape s;       // ❌ 错误,抽象类不能实例化
Shape* p = new Circle();
p->draw();        // 输出:Drawing Circle

🔸 若 Circle 不实现 draw(),它也将变为抽象类。

接口类(interface)

C++ 没有 interface 关键字,但你可以用纯虚函数模拟接口类:

cpp 复制代码
class IStream {
public:
    virtual void read() = 0;
    virtual void write() = 0;
    virtual ~IStream() {}  // 接口类应定义虚析构
};

6、Lambda 表达式(C++11 起)

cpp 复制代码
[捕获列表](参数列表) -> 返回类型 {
    函数体
}

其中:

  • []:捕获列表(可以捕获外部变量)
  • ():参数列表
  • ->:返回类型(可省略)
  • {}:函数体

匿名函数,通常用于简洁回调:

cpp 复制代码
auto add = [](int a, int b) -> int {
    return a + b;
};
std::cout << add(3, 5);  // 输出:8

//返回类型如果能推导,可以省略 `-> int`:
auto add = [](int a, int b) {
    return a + b;
};
std::cout << add(3, 4);  // 输出 7

Lambda 表达式是 C++11 引入的一种匿名函数 ,用于定义可内联的函数对象,特别适合临时、小巧的函数使用场景,如算法回调、事件处理、线程创建等。

捕获外部变量(capture)

捕获方式 示例 说明
值捕获 [x] 捕获变量 x 的值(拷贝)
引用捕获 [&x] 捕获变量 x 的引用
捕获全部(值) [=] 捕获所有外部变量的值
捕获全部(引用) [&] 捕获所有外部变量的引用
混合捕获 [=, &y] y 外其他变量值捕获
cpp 复制代码
int x = 10;
int y = 5;

auto f = [=, &y]() {
    // x 是值捕获,y 是引用捕获
    std::cout << x + y << "\n";
    y += 1;  // 允许修改 y
};

f();

常见应用场景

  1. 与 STL 算法结合(如 std::sort
cpp 复制代码
std::vector<int> v = {4, 2, 5, 1};
std::sort(v.begin(), v.end(), [](int a, int b) {
    return a < b;
});
  1. 与线程一起使用:
cpp 复制代码
#include <thread>
std::thread t([] {
    std::cout << "In thread\n";
});
t.join();

可变 lambda(mutable

默认情况下,值捕获的变量是不可修改的。加上 mutable 可以让其变为可变:

cpp 复制代码
int x = 5;
auto f = [x]() mutable {
    x += 1;       // 允许修改捕获变量的副本
    std::cout << x;
};
f();  // 输出 6,但外部 x 不变

特殊函数类型

类型 用途
构造函数 创建对象时自动调用
析构函数 对象销毁时自动调用
拷贝构造函数 对象以另一个对象初始化时调用
移动构造函数 右值初始化对象时调用
运算符重载函数 重载 +, ==
虚函数 用于多态
纯虚函数 抽象类成员函数

二、函数参数值传递与引用传递

在 C++ 中,函数参数 默认是值传递(pass-by-value) ,但 并不都是值传递,C++ 支持多种参数传递方式,主要包括以下几种:


1. 值传递(Pass by Value)

  • 将实参的副本传入函数
  • 函数内对参数的修改不会影响原变量
cpp 复制代码
void foo(int x) {
    x = 100;
}

int main() {
    int a = 10;
    foo(a);
    std::cout << a; // 输出 10,不变
}

2. 引用传递(Pass by Reference)

  • 传入变量的别名,函数内对其修改会影响原变量
cpp 复制代码
void foo(int& x) {
    x = 100;
}

int main() {
    int a = 10;
    foo(a);
    std::cout << a; // 输出 100,被修改了
}

3. 指针传递(Pass by Pointer)

  • 函数接收变量的地址,通过指针访问和修改
cpp 复制代码
void foo(int* x) {
    *x = 100;
}

int main() {
    int a = 10;
    foo(&a);
    std::cout << a; // 输出 100
}

4. 常引用传递(Pass by const Reference)

  • 适用于避免拷贝开销 ,但又不允许函数修改实参
  • 常用于传递大型对象,如 std::string, std::vector
cpp 复制代码
void print(const std::string& s) {
    std::cout << s;
}

5. 右值引用(Pass by rvalue reference)C++11+

  • 支持移动语义,避免不必要的深拷贝
cpp 复制代码
void foo(std::string&& s) {
    std::cout << s;
}

foo("hello"s);  // 移动传参

小结对照表:

方式 是否复制 是否可修改原变量 适用场景
值传递 ✅ 是 ❌ 否 小数据类型(int, float)
引用传递 ❌ 否 ✅ 是 需要修改原变量
指针传递 ❌ 否 ✅ 是 类似引用,但更灵活
const 引用传递 ❌ 否 ❌ 否 传大型对象且不修改
右值引用传递 ❌ 否 ✅ 是 支持移动,避免拷贝

结论:

C++ 中函数参数默认是值传递 ,但你可以通过 &(引用)、*(指针)或 &&(右值引用)来实现其他传参方式。

三、构造函数 析构函数

构造函数Constructor

构造函数是当对象被创建 时自动调用的特殊函数,用于初始化对象的成员变量

  • 名字与类名相同
  • 没有返回值
  • 可以有多个
类型 用途
默认构造函数 不带参数或所有参数有默认值
带参构造函数 用户提供初始化参数
拷贝构造函数 用已有对象创建新对象(传值方式)
移动构造函数(C++11) 用于资源"窃取"(效率更高)
委托构造函数(C++11) 在一个构造函数中调用另一个构造函数

拷贝构造函数 Copy Constructor

如果你没有显式定义 operator=(),C++ 会默认生成一个浅拷贝的赋值运算符,对每个成员做成员赋值。但如果你类中包含裸指针等资源,默认赋值将产生浅拷贝问题(资源共享,析构冲突),此时应自定义赋值运算符。

拷贝构造作用:

  • 通过一个已有对象初始化另一个对象
  • 将对象按值传递给函数
  • 函数按值返回对象
cpp 复制代码
ClassName(const ClassName& other);

参数是 const & 避免递归调用自身

禁止拷贝构造:

cpp 复制代码
// 在 C++11/14/17 中,推荐使用 `= default` 和 `= delete` 明确指定:
class MyClass {
public:
    MyClass() = default;
    MyClass(const MyClass&) = delete;
    ~MyClass() = default;
};

析构函数 Destructor

析构函数是在对象销毁时自动调用 的特殊函数,用于释放资源、关闭文件、清理指针等

  • 名字为 ~类名
  • 没有参数,没有返回值
  • 每个类最多只能有一个析构函数
  • 可以是虚的(用于多态删除)

示例

将定义和实现全部写在头文件中的写法:

cpp 复制代码
#include <iostream>
#include <string>

class Person {
public:
    std::string name;
    int* age;

    // ✅ 1. 默认构造函数(委托给带参构造)
    Person() : Person("unknown", 0) {
        std::cout << "Default constructor called (delegated)\n";
    }

    // ✅ 2. 带参构造函数
    Person(const std::string& name_, int age_) {
        name = name_;
        age = new int(age_);
        std::cout << "Parameterized constructor called\n";
    }

    // ✅ 3. 拷贝构造函数(深拷贝)
    Person(const Person& other) {
        name = other.name;
        age = new int(*other.age);
        std::cout << "Copy constructor called\n";
    }

    // ✅ 4. 移动构造函数(C++11)
    Person(Person&& other) noexcept {
        name = std::move(other.name); // string 自带 move
        age = other.age;              // 窃取指针
        other.age = nullptr;          // 避免析构 double free
        std::cout << "Move constructor called\n";
    }

    // ✅ 5. 析构函数
    ~Person() {
        std::cout << "Destructor called for " << name << "\n";
        delete age;
    }
};

现代c++推荐头文件和源文件分离,分离的写法:

cpp 复制代码
// Person.h
#ifndef PERSON_H
#define PERSON_H

#include <string>

class Person {
public:
    std::string name;
    int* age;
	// ✅ 1.默认构造函数
    Person();
    // ✅ 2.带参构造函数
    Person(const std::string& name_, int age_);
    // ✅ 3.拷贝构造函数(深拷贝)
    Person(const Person& other);
    // ✅ 4.移动构造函数(C++11)
    Person(Person&& other) noexcept;
    // ✅ 5.析构函数
    virtual ~Person();
    
	// 拷贝赋值运算符(可选)
    Person& operator=(const Person& other);
    // 移动赋值运算符(可选)
    Person& operator=(Person&& other) noexcept;

    // ✅ const表示该函数不会修改类成员变量,如果在常函数里修改成员变量会报错
    virtual void introduce() const;
};

#endif
cpp 复制代码
// Person.cpp
#include "Person.h"
#include <iostream>

// ✅ 1.默认构造函数
Person::Person() : Person("unknown", 0) {
    std::cout << "Default constructor called\n";
}

// ✅ 2.带参构造函数
Person::Person(const std::string& name_, int age_) {
    name = name_;
    age = new int(age_);
    std::cout << "Parameterized constructor called\n";
}

// ✅ 3.拷贝构造函数(深拷贝)
Person::Person(const Person& other) {
    name = other.name;
    age = new int(*other.age);
    std::cout << "Copy constructor called\n";
}

// ✅ 4.移动构造函数(C++11)
Person::Person(Person&& other) noexcept {
    name = std::move(other.name);
    age = other.age;
    other.age = nullptr;
    std::cout << "Move constructor called\n";
}

// ✅ 5.析构函数
Person::~Person() {
    std::cout << "Person destructor called for " << name << "\n";
    delete age;
}

// 拷贝赋值运算符(可选)
Person& Person::operator=(const Person& other) {
    if (this != &other) {
        name = other.name;
        delete age;
        age = new int(*other.age);
    }
    return *this;
}

// 移动赋值运算符(可选)
Person& Person::operator=(Person&& other) noexcept {
    if (this != &other) {
        name = std::move(other.name);
        delete age;
        age = other.age;
        other.age = nullptr;
    }
    return *this;
}

void Person::introduce() const {
    std::cout << "Hi, I am " << name << ", age " << *age << ".\n";
}

默认构造/拷贝/析构行为总结

函数类型 是否自动生成 什么时候需要自定义?
构造函数 ✅(如果没写) 成员需要特殊初始化逻辑时
拷贝构造函数 ✅(如果没写) 含指针资源、句柄或禁止拷贝
析构函数 ✅(如果没写) 成员包含动态资源(如 new)时需释放
移动构造函数 ❌(C++11+) 优化效率或防止拷贝
  • 如果你不写 ,编译器会自动生成一个"浅拷贝"版本。
  • 如果类中有裸指针 ,一定要自己写拷贝构造,否则可能引发双重释放错误。

深拷贝和浅拷贝

C++ 中的 深拷贝(deep copy)浅拷贝(shallow copy) 是对象复制时的两种方式,区别在于是否真正复制了堆上资源。理解这两者对掌握类的构造函数、拷贝构造函数和析构函数至关重要。

一、定义和区别

特性 浅拷贝(Shallow Copy) 深拷贝(Deep Copy)
拷贝内容 只复制指针的地址 分配新内存并复制数据内容
资源共享 原对象和副本指向同一内存 原对象和副本各自拥有独立内存
安全性 ❌ 改变一个对象会影响另一个;易发生悬垂指针、双重释放 ✅ 对象互不影响
析构风险 ❌ 多次析构同一块内存(如果未正确管理) ✅ 每个对象析构各自拥有的内存

二、示例演示

cpp 复制代码
#include <iostream>
#include <cstring>

class Person {
public:
    char* name;

    // 构造函数
    Person(const char* n) {
        name = new char[strlen(n) + 1];
        strcpy(name, n);
    }

    // 浅拷贝构造函数(默认)
    // Person(const Person& other) = default;

    // ✅ 深拷贝构造函数
    Person(const Person& other) {
        name = new char[strlen(other.name) + 1];
        strcpy(name, other.name);
    }

    // 析构函数
    ~Person() {
        delete[] name;
    }

    void print() {
        std::cout << "Name: " << name << std::endl;
    }
};

如果不写深拷贝构造函数,编译器默认使用浅拷贝,即只是复制了指针 name 的地址,两个对象共用同一块堆内存。这样一来,修改 p2.name 会影响 p1.name,两个对象析构时还会重复释放同一块内存,导致崩溃。


三、默认拷贝行为说明

操作类型 默认行为 是否安全
拷贝构造函数 浅拷贝
赋值运算符 = 浅赋值
析构函数 默认释放 ❌(若使用裸指针)

const在类中的作用

const 可以修饰类的成员函数函数参数与返回值 和对象,但它不能直接修饰整个类本身


const 修饰函数(常成员函数)

cpp 复制代码
class MyClass {
public:
    int getValue() const; // ✅ 表示该函数不会修改类成员变量
private:
    int value = 42;
};
int MyClass::getValue() const {
    // this->value = 10; ❌ 编译错误:不能修改成员变量
    return value;
}
cpp 复制代码
const MyClass obj;   // ✅ **只能调用 const 成员函数**
MyClass obj2;        // ✅ **既可以调用 const 成员函数,也可以调用非常成员函数**

const 修饰函数参数和返回值

修饰参数

cpp 复制代码
void printName(const std::string& name); // ✅ 避免拷贝 + 保证不修改参数

修饰返回值

cpp 复制代码
const std::string& getName() const; // ✅ 返回值不能被修改(防止误用)

注意:const 修饰返回值 时,通常用于返回引用或指针,不太常用于值返回。


不能直接修饰类

cpp 复制代码
const class MyClass {}; // ❌ 不常见,基本无意义

四、继承

基类函数用 virtual 修饰,子类可以 override

继承方式 修饰符可见性

c++ Java中的继承方式是完全一样的,继承方式

继承方式 基类的 public 成员在子类中变成 基类的 protected 成员在子类中变成 基类的 private 成员
public 继承 public protected ❌ 不可访问
protected 继承 protected protected ❌ 不可访问
private 继承 private private ❌ 不可访问

可见性

修饰符 类内访问 派生类访问 类外访问
public ✅ 可访问 ✅ 可访问 ✅ 可访问
protected ✅ 可访问 ✅ 可访问 ❌ 不可访问
private ✅ 可访问 ❌ 不可访问 ❌ 不可访问

示例

cpp 复制代码
//student.h
#ifndef STUDENT_H
#define STUDENT_H

#include "Person.h" 

class Student : public Person {
public:
    std::string school;

    Student();
    Student(const std::string& name_, int age_, const std::string& school_);
    ~Student() override;

    void introduce() const override;
};

#endif
cpp 复制代码
//student.cpp
#include "Student.h"
#include <iostream>

Student::Student() : Person("unknown_student", 18), school("Unknown School") {
    std::cout << "Student default constructor\n";
}

Student::Student(const std::string& name_, int age_, const std::string& school_)
    : Person(name_, age_), school(school_) {
    std::cout << "Student parameterized constructor\n";
}

Student::~Student() {
    std::cout << "Student destructor called for " << name << "\n";
}

void Student::introduce() const {
    std::cout << "Hi, I am student " << name << ", age " << *age
              << ", studying at " << school << ".\n";
}

构造函数和析构函数的顺序

  • 构造顺序:先构造基类 → 再构造派生类
  • 析构顺序:先析构派生类 → 再析构基类
cpp 复制代码
class Base {
public:
    Base() { std::cout << "Base()\n"; }
    ~Base() { std::cout << "~Base()\n"; }
};

class Derived : public Base {
public:
    Derived() { std::cout << "Derived()\n"; }
    ~Derived() { std::cout << "~Derived()\n"; }
};

输出顺序:

复制代码
Base()
Derived()
~Derived()
~Base()

C++特有:多继承与虚继承 菱形继承问题

多继承

cpp 复制代码
class A { public: int x; };
class B { public: int x; };
class C : public A, public B {};  // 多继承,C 有两个 x,需区分 A::x 和 B::x

"菱形继承问题(Diamond Problem) "是 C++ 多继承中特有的一种继承结构冲突问题,会导致:

  1. 基类子对象重复
  2. 数据二义性 / 歧义访问
  3. 构造和析构混乱
  4. 资源浪费

一、什么是菱形继承结构?

如下图所示,BC 都继承自 A,而 D 同时继承自 BC

cpp 复制代码
      A
     / \
    B   C
     \ /
      D
class A {
public:
    int value;
};

class B : public A {};
class C : public A {};
class D : public B, public C {};

二、菱形继承引发的问题

1. 产生 多个 A 子对象

D 中包含了 两个 A 对象

  • 一个来自 B(B→A)
  • 一个来自 C(C→A)

2. 访问 value 发生歧义

cpp 复制代码
D d;
d.value = 10; // ❌ 错误:编译器不知道是 B::A::value 还是 C::A::value

必须手动指定:

cpp 复制代码
d.B::value = 10;
d.C::value = 20;

三、解决方法:虚继承(virtual

virtual 修饰继承关系,让 B 和 C 共享同一个 A 子对象

cpp 复制代码
class A {
public:
    int value;
};

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

虚继承效果:

  • D 中只有 一个 A 对象,由编译器自动协调
  • 访问成员不再歧义:
cpp 复制代码
D d;
d.value = 10; // ✅ 正常访问,无歧义

五、友元

在 C++ 中,友元(friend) 机制允许非成员函数或其他类访问某个类的私有(private)和受保护(protected)成员。友元关系是 单向不传递 的,常用于操作符重载、调试工具或两个类之间的紧密协作等场景。


一、友元的三种形式

1. 友元函数(Friend Function)

cpp 复制代码
class Box {
private:
    int width;
public:
    Box(int w) : width(w) {}

    // 声明友元函数
    friend void printWidth(const Box& b);
};

// 非成员函数,可以访问 Box 的私有成员
void printWidth(const Box& b) {
    std::cout << "Width: " << b.width << std::endl;
}

🔸特点

  • 非类成员函数,但拥有类的访问权限
  • 常用于重载 operator<< 等操作符

2. 友元类(Friend Class)

cpp 复制代码
class Engine;

class Car {
private:
    int speed;
public:
    Car(int s) : speed(s) {}
    friend class Engine;  // Engine 可以访问 Car 的私有成员
};

class Engine {
public:
    void showSpeed(const Car& c) {
        std::cout << "Speed: " << c.speed << std::endl;
    }
};

🔸特点

  • 一个类可以将另一个类声明为友元
  • 该友元类的所有成员函数都能访问被友元类的私有成员
  • 单向:Engine 是 Car 的朋友,但 Car 不可访问 Engine 的私有成员

3. 成员函数作为友元

cpp 复制代码
class B;

class A {
private:
    int a_val;
public:
    A(int v) : a_val(v) {}
    friend void B::printA(const A& a);  // 只让 B 的某个成员函数成为友元
};

class B {
public:
    void printA(const A& a) {
        std::cout << a.a_val << std::endl;
    }
};

注意:这种方式必须 先声明 B 类 ,否则编译器不知道 B::printA 是什么。


二、友元的特性总结

特性 说明
单向访问 被声明为友元的类/函数可以访问声明类的私有成员,反之不行
非成员也可声明为友元 普通函数、类、类的成员函数都可以作为友元
不破坏封装性 尽管能访问私有成员,但访问范围被显式限定
编译时绑定 友元关系在编译时建立,无法在运行时动态设置
不继承、不传递 子类不会继承友元权限,友元类的友元也无访问权

三、示例:重载 << 运算符

cpp 复制代码
class Person {
private:
    std::string name;
    int age;
public:
    Person(std::string n, int a) : name(n), age(a) {}
    friend std::ostream& operator<<(std::ostream& os, const Person& p);
};

std::ostream& operator<<(std::ostream& os, const Person& p) {
    os << "Name: " << p.name << ", Age: " << p.age;
    return os;
}