C++开发:类的基础

一、构造函数

构造函数是类的一种特殊成员函数,用于 对象创建时初始化对象的数据成员。

特点:

  • 名字与类名相同。
  • 没有返回值(连 void 也不写)。
  • 可以被重载
  • 在对象创建时自动调用。

示例:

#include <iostream>
using namespace std;

class Point {
private:
    int x;
    int y;
public:
    // 默认构造函数
    Point() {
        x = 0;
        y = 0;
    }

    // 带参数构造函数
    Point(int xVal, int yVal) {
        x = xVal;
        y = yVal;
    }

    void print() {
        cout << "(" << x << ", " << y << ")" << endl;
    }
};

int main() {
    Point p1;          // 调用默认构造函数
    Point p2(3, 4);    // 调用带参数构造函数

    p1.print();  // (0, 0)
    p2.print();  // (3, 4)
}

构造函数的分类:

  • 默认构造函数:无参数或所有参数都有默认值。
  • 带参数构造函数:用于初始化对象。
  • 拷贝构造函数:ClassName(const ClassName &other),用于用已有对象初始化新对象。
  • 移动构造函数(C++11):ClassName(ClassName&& other),用于移动语义优化。

1.1 explicit 关键字

explicit 用于 修饰构造函数,阻止构造函数被 隐式类型转换 调用。

为什么需要 explicit?
在 C++ 中,如果构造函数只有一个参数,它可以被用作隐式类型转换:

class Foo {
public:
    Foo(int a) { cout << "Foo(int) called" << endl; }
};

void printFoo(Foo f) { }

int main() {
    printFoo(10);  // 隐式转换:int -> Foo
}

这里 10 会隐式转换成 Foo(10),有时会导致不安全或难以发现的 bug。

使用 explicit 可以禁止隐式转换:

class Foo {
public:
    explicit Foo(int a) { cout << "Foo(int) called" << endl; }
};

int main() {
    // printFoo(10); // 错误,禁止隐式转换
    printFoo(Foo(10)); // 正确,显示转换
}

总结:

  • 单参数构造函数如果不加 explicit,可能会被当作类型转换。
  • 建议 能加 explicit 就加,提高代码安全性。

### 1.2 构造函数初始化列表
初始化列表是一种在构造函数体执行前 直接初始化成员变量的语法:

class Point {
private:
    int x;
    int y;
public:
    // 初始化列表写法
    Point(int a, int b) : x(a), y(b) { }

    void print() {
        cout << "(" << x << ", " << y << ")" << endl;
    }
};

为什么使用初始化列表?

  1. 效率更高:如果成员是对象,初始化列表直接调用构造函数,而在构造函数体里赋值会先调用默认构造再赋值。
  2. 常量成员、引用成员必须使用初始化列表
class Test {
    const int a;
    int &b;
public:
    Test(int val, int &ref) : a(val), b(ref) { }
};

const 和 引用 不能在构造函数体内赋值。

  1. 支持基类构造函数调用:
class Base {
public:
    Base(int v) { cout << "Base " << v << endl; }
};

class Derived : public Base {
public:
    Derived(int x) : Base(x) { cout << "Derived " << x << endl; }
};

完整示例:

#include <iostream>
using namespace std;

class Point {
private:
    const int x;
    int &y;
public:
    explicit Point(int a, int &bRef) : x(a), y(bRef) {
        cout << "Point constructed" << endl;
    }

    void print() {
        cout << "(" << x << ", " << y << ")" << endl;
    }
};

int main() {
    int val = 20;
    Point p(val, val);
    p.print();
    // Point p2 = 10; // 错误,explicit 禁止隐式转换
}

二、对象拷贝

2.1 概念

对象拷贝(Object Copy)是指用一个已有对象去创建或赋值另一个对象的过程。
主要分为两种情况:

拷贝构造:用一个对象来初始化另一个对象。

MyClass obj1;       // 已有对象
MyClass obj2(obj1); // 调用拷贝构造函数

拷贝赋值:将一个对象赋值给另一个已经存在的对象。

MyClass obj1;
MyClass obj2;
obj2 = obj1; // 调用拷贝赋值运算符

2.2 默认的拷贝行为

如果没有显式定义拷贝构造函数和拷贝赋值运算符,C++ 会 自动生成默认版本:

  • 默认拷贝构造函数:逐个成员执行浅拷贝(bitwise copy)。
  • 默认拷贝赋值运算符:同样是逐个成员的浅拷贝。

这对大多数内置类型和简单类是安全的,但对于动态分配内存(指针)的类,浅拷贝可能会导致:

  • 双重释放(double free)
  • 悬挂指针(dangling pointer)

2.3 自定义拷贝构造函数和拷贝赋值运算符

如果类中有指针或需要特殊处理的资源,就应该手动实现。

示例:深拷贝

#include <iostream>
#include <cstring>
using namespace std;

class MyString {
private:
    char* data;
public:
    // 构造函数
    MyString(const char* str = "") {
        data = new char[strlen(str) + 1];
        strcpy(data, str);
    }

    // 拷贝构造函数(深拷贝)
    MyString(const MyString& other) {
        data = new char[strlen(other.data) + 1];
        strcpy(data, other.data);
        cout << "拷贝构造函数被调用" << endl;
    }

    // 拷贝赋值运算符(深拷贝)
    MyString& operator=(const MyString& other) {
        if (this == &other) return *this; // 防止自赋值

        delete[] data; // 释放原内存
        data = new char[strlen(other.data) + 1];
        strcpy(data, other.data);
        cout << "拷贝赋值运算符被调用" << endl;
        return *this;
    }

    // 析构函数
    ~MyString() {
        delete[] data;
    }

    void print() const {
        cout << data << endl;
    }
};

int main() {
    MyString s1("Hello");
    MyString s2 = s1;   // 调用拷贝构造函数
    MyString s3;
    s3 = s1;            // 调用拷贝赋值运算符

    s1.print();
    s2.print();
    s3.print();
}

2.4 拷贝控制成员

通常与对象拷贝相关的函数有:

  • 拷贝构造函数
  • 拷贝赋值运算符
  • 析构函数
  • 移动构造函数
  • 移动赋值运算符

2.5 对象拷贝过程的内存图

在这里插入图片描述

三、关键字 inline

3.1 什么是 inline?

  在 C++ 中,inline 的主要作用是 向编译器建议:在调用函数的地方直接展开函数体,而不是生成普通函数调用。
也就是说,inline 试图消除函数调用开销(参数压栈、跳转、返回)。

但是要注意:

  • 它只是“建议(hint)”,编译器可以忽略。
  • 现代编译器有强大的优化器,即使你不写 inline,在优化时也可能会自动内联。

内联的适用场景:

  • 短小函数(如 getter、setter、数学小函数)。
  • 频繁调用的函数。
  • 模板函数(通常定义在头文件里)。

不适合用 inline 的情况:

  • 函数体很大(代码膨胀,反而性能下降)。
  • 递归函数(编译器通常不会内联递归)。
  • 虚函数(运行时多态,不能真正内联)。

底层实现原理:
调用一个普通函数时,底层一般会做:

  1. 参数入栈(或寄存器传递)。
  2. 保存返回地址。
  3. 跳转到函数体执行。
  4. 执行 ret 返回。
  5. 清理栈。

内联函数的编译器处理:
当函数被 inline 后,编译器在 编译阶段 可能会直接把函数体展开到调用处,例如:
源代码:

int y = add(2, 3) + add(4, 5);

编译器可能展开为:

int y = (2 + 3) + (4 + 5);

与宏的区别:

  • 宏展开发生在 预处理阶段,没有类型检查,可能产生难以发现的 bug。
  • inline 发生在 编译阶段,有严格的类型检查,更安全。

例如:

#define SQUARE(x) (x * x)   // 宏
inline int square(int x) { return x * x; } // inline函数

调用 SQUARE(1+2) → 展开成 (1+2*1+2),结果是 5 而不是 9。而 square(1+2) 结果就是正确的 9。

现代编译器的优化:

  • 编译器(如 GCC/Clang/MSVC)即使你不写 inline,在 O2/O3 优化模式下,也会自动决定是否内联。
  • 相反,即使你写了 inline,如果函数太大,编译器也可能拒绝内联。
  • 所以 inline 更多用于 解决多重定义问题,而不是“强制内联”。

3.2 基本用法

示例 1:普通函数内联

#include <iostream>
using namespace std;

inline int add(int a, int b) {
    return a + b;
}

int main() {
    cout << add(2, 3) << endl;
    return 0;
}

编译器可能会把 add(2,3) 替换成:

cout << 2 + 3 << endl;

这样就避免了函数调用开销。

示例 2:类内成员函数(默认内联)

class Foo {
public:
    int x;
    int getX() const { return x; }  // 默认inline
};

在类定义体里写的成员函数,默认就是 inline。

四、const 的基本作用

const 表示 常量 或 不可修改。在 C++ 中,它用于限制变量、指针、函数参数、成员函数等的可修改性。
主要目的:

  • 提高代码安全性,防止误修改。
  • 提高可读性,明确设计意图。
  • 便于编译器优化。

4.1 常见用法

修饰普通变量

const int a = 10;
a = 20; // ❌ 错误,a 是只读的

修饰指针
分为三种情况:

int x = 10, y = 20;
const int* p1 = &x;   // 指向的值不可改(内容只读)
int* const p2 = &x;   // 指针本身不可改(地址只读)
const int* const p3 = &x; // 值和指针都不可改
  • const int* p:不能通过 p 修改 *p 的值,但可以改变 p 指向谁。
  • int* const p:不能改变 p 的指向,但可以修改指向对象的值。
  • const int* const p:都不能改。

修饰函数参数

void foo(const int x);        // 形参 x 在函数内不可修改
void bar(const int* p);       // 不能通过 p 改变 *p
void baz(const int& r);       // 防止引用修改,常用来避免拷贝

典型场景:传大对象时用 const &:避免拷贝,又保证只读

void print(const std::string& s) {
    std::cout << s << std::endl;
}

修饰函数返回值

const int foo();        // 返回值不可被修改(通常作用有限)
const int& bar();       // 返回引用,但只读
const int* baz();       // 返回只读指针

例如:

const std::string& getName() const; 

修饰类成员函数

class Test {
    int value;
public:
    int getValue() const {   // const成员函数
        return value;        // ✅ 只能读
        // value = 10; ❌ 错误,不能修改成员变量
    }
};

const 成员函数保证 不修改类的成员变量(除 mutable 成员外)。

与 mutable 结合
mutable 成员即使在 const 函数中也能修改:

class Test {
    mutable int cache;
public:
    int get() const {
        cache++;   // ✅ 即使是 const 成员函数,也能改
        return cache;
    }
};

4.2 底层实现原理

  • const 在 编译期生效,它主要是 编译器的语义检查,保证不被错误修改。
  • 编译器会在符号表中标记变量为只读,一旦出现修改行为,直接报编译错误。
  • 对于 const 全局变量,编译器可能会将其放入只读数据段(.rodata),在运行期修改会导致段错误。
  • const 成员函数的底层实现:编译器会在函数签名中加一个额外的 this 指针限定:
void foo() const; 
// 相当于:
void foo(const Test* const this);

所以它保证了 this 指针指向的对象不可修改。

五、this 指针

5.1 this 指针的由来

在 C 语言里,没有类(class),函数只是操作结构体数据的普通函数。例如:

struct Point { int x, y; };

void move(Point* p, int dx, int dy) {
    p->x += dx;
    p->y += dy;
}

调用时必须显式传递结构体地址 move(&pt, 1, 2)。

而到了 C++,为了支持面向对象编程(把数据和操作绑定),就需要能在成员函数里隐式访问所属的对象。这时候就引入了一个隐藏的指针:this。它自动指向调用该成员函数的对象。

5.2 this 指针的基本概念

  • this 是一个 隐含在所有非静态成员函数中的指针参数。
  • 它指向调用该成员函数的对象本身。
  • 对于类 X,在成员函数中 this 的类型是:普通成员函数:X* const this;常量成员函数:const X* const this

5.3 this 的作用

区分成员变量和局部变量

class A {
    int x;
public:
    void setX(int x) {
        this->x = x;  // 区分成员变量和参数
    }
};

支持链式调用

class A {
    int x;
public:
    A& set(int v) { x = v; return *this; }
};

A a;
a.set(5).set(10).set(20);  // 链式调用

在运算符重载中返回当前对象

class A {
    int x;
public:
    A(int v):x(v){}
    A& operator++() {
        x++;
        return *this;
    }
};

区分对象:

A a, b;
a.set(10);   // this 指向 a
b.set(20);   // this 指向 b

5.4 底层实现原理

编译器会在成员函数里偷偷加上一个隐含参数 this,本质上就是指针传参。

class A {
    int x;
public:
    void setX(int v) { x = v; }
};

编译器会转化为类似 C 形式:

struct A { int x; };

void setX(A* this, int v) {
    this->x = v;
}
  • this 并不是语法糖,它是编译器在成员函数中自动传递的对象地址。
  • static 成员函数没有 this,因为它不依赖对象。

六、关键字 static

6.1 static 的作用概览

在 C++ 中,static 有不同的语义,取决于它出现的位置:

  • 函数内部的局部变量:使局部变静态存储,即变量只在第一次执行时初始化,并且在整个程序运行过程中一直存在(直到程序结束)。生命周期变长,但作用域仍然局限在函数内部。
  • 全局/命名空间作用域的变量或函数:限制其作用域仅在当前编译单元(.cpp 文件)内可见,避免符号冲突(内部链接)。
  • 类中的成员变量(静态成员变量):属于整个类共享,而不是某个对象独有;生命周期从程序开始到结束。必须在类外进行定义(如果是非 constexpr)。
  • 类中的成员函数(静态成员函数):不依赖于对象实例,可以直接通过类名调用;不能访问类的非静态成员,只能访问静态成员。

使用示例:
函数内部的 static 局部变量

#include <iostream>
using namespace std;

void counter() {
    static int count = 0; // 只初始化一次
    count++;
    cout << "调用次数: " << count << endl;
}

int main() {
    counter(); // 调用次数: 1
    counter(); // 调用次数: 2
    counter(); // 调用次数: 3
}

这里的 count 会在整个程序运行期间一直存在,而不是每次调用 counter() 都重新创建。

全局变量或函数的 static

// file1.cpp
static int g_value = 10; // 只在 file1.cpp 内可见

static void helper() {   // 只在 file1.cpp 内可见
    cout << "helper in file1.cpp" << endl;
}

// file2.cpp 无法访问 g_value 和 helper()

类的静态成员变量

#include <iostream>
using namespace std;

class MyClass {
public:
    static int s_value; // 声明静态成员
};

int MyClass::s_value = 100; // 类外定义

int main() {
    cout << MyClass::s_value << endl; // 100
    MyClass obj1, obj2;
    obj1.s_value = 200;
    cout << obj2.s_value << endl; // 200,所有对象共享
}

静态成员变量存放在全局区,而不是对象实例中。

类的静态成员函数

#include <iostream>
using namespace std;

class MyClass {
public:
    static void show() {
        cout << "Static function called!" << endl;
    }
};

int main() {
    MyClass::show(); // 通过类调用
    MyClass obj;
    obj.show();      // 也可以通过对象调用,但不依赖对象
}

static 的总结:

  • 局部变量:延长生命周期,但作用域不变。
  • 全局变量/函数:限制在当前编译单元可见,避免冲突。
  • 类的静态成员变量:类级别共享,需在类外定义。
  • 类的静态成员函数:不依赖对象,不能访问非静态成员。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值