C++杂记

类内初始化

class Person{
public:
    // static member shared by all obj.
    // memory was allocated during compilation phase
    static int m_a = 0;
    //error:a member with an in-class initializer must be const

    int a = 20;
    
};

why the member “int a” in class can be init,but the static member "static int m_a"can’t be init in class?

Great question!
In C++, non-static data members (like int a = 20;) can be initialized directly in the class definition (since C++11). This is called in-class member initialization.
However, static data members (like static int m_a;) are shared by all instances of the class and exist independently of any object.
They must be defined and initialized outside the class (in a source file), because:

  • The class definition only declares the static member.
  • The actual storage for the static member must be allocated in exactly one translation unit (usually the .cpp file).
    静态成员变量,类内(只是)声明,类外(才)定义

1. 什么是 Translation Unit(翻译单元)?

Translation Unit(翻译单元) 是 C/C++ 编译过程中的一个基本概念。
它指的是编译器实际编译的源代码单元,由以下部分组成:

  • 一个源文件(通常是 .c.cpp 文件)
  • 该源文件中包含的所有头文件(通过 #include 指令展开后的内容)
  • 以及所有宏展开和预处理后的内容

换句话说,一个源文件连同它所有直接或间接包含的头文件,经过预处理后形成的整体,就是一个翻译单元。


2. 为什么需要“一个翻译单元”来分配静态成员变量的存储?

对于类的静态成员变量,声明通常写在类定义里(头文件中的 static 声明),
静态成员变量属于类的全局共享变量,必须在某个翻译单元中定义一次, 以分配实际的内存

如果你在多个翻译单元中都定义了静态成员变量,链接时会报重复定义错误;
如果没有定义,会报链接错误。


3. 举例说明

示例:

头文件 MyClass.h

#ifndef MYCLASS_H
#define MYCLASS_H

class MyClass {
public:
    static int count;  // 声明,不分配内存
    MyClass() { ++count; }
};

#endif

源文件 MyClass.cpp

#include "MyClass.h"

int MyClass::count = 0;  // 定义并分配内存,属于这个翻译单元

其他文件 main.cpp

#include <iostream>
#include "MyClass.h"

int main() {
    MyClass a;
    MyClass b;
    std::cout << MyClass::count << std::endl;  // 输出 2
    return 0;
}
  • MyClass.h 只是声明了 static int count,没有分配内存。
  • MyClass.cppcount 进行了定义和初始化,分配了实际内存。
  • 编译时,MyClass.cppmain.cpp 各自是一个翻译单元。
  • 链接时,count 的内存只从 MyClass.cpp 这一个翻译单元分配,避免重复定义。

4. 总结

术语说明
翻译单元 (TU)预处理后,一个源文件及其包含的所有头文件组成的整体
静态成员变量的定义必须放在某一个翻译单元中,进行实际内存分配
多个翻译单元共享同一个静态成员变量的定义,链接器负责合并

5. 拓展

  • C++17 引入了 inline static 变量,可以放在头文件里,允许在多个翻译单元中定义而不冲突,编译器和链接器负责合并。
  • 传统写法仍然需要在一个 .cpp 文件中定义静态成员变量。


空类的大小

// sizeof($empty_class) = 1
// each object should have a unique address in memory.
class Myclass
{

};

void test01()
{
    std::cout<<"sizeof(Myclass):"<<sizeof(Myclass)<<"\t(Empty class)\n";
    Myclass myclass;
    std::cout<<"sizeof(myclass):"<<sizeof(myclass)<<"\t(obj of Empty class)\n";
    Myclass arr[3];
    
}

空类(的对象)的大小为 1 byte(上述代码结果如下)

sizeof(Myclass):1       (Empty class)
sizeof(myclass):1       (obj of Empty class)

每个对象都应该有一个独一无二的内存地址
在这里插入图片描述
上图中,空类构造的数组中成员的地址如下:

&arr[0] = 0x7fffffffd435
&arr[1] = 0x7fffffffd435
&arr[2] = 0x7fffffffd437

类对象模型

static成员并不属于类的对象

eg1.

class Myclass
{
public:
    int m_a;
    // static int m_s;

};
// int Myclass::m_s = 0;

void test01()
{
    std::cout<<"sizeof(Myclass):"<<sizeof(Myclass)<<"\t(class)\n";
    Myclass myclass;
    std::cout<<"sizeof(myclass):"<<sizeof(myclass)<<"\t(obj of class)\n";
    
}
sizeof(Myclass):4       (class)
sizeof(myclass):4       (obj of class)

eg2.

class Myclass
{
public:
    int m_a;
    static int m_s;

};
int Myclass::m_s = 0;

void test01()
{
    std::cout<<"sizeof(Myclass):"<<sizeof(Myclass)<<"\t(class)\n";
    Myclass myclass;
    std::cout<<"sizeof(myclass):"<<sizeof(myclass)<<"\t(obj of class)\n";
    
}
sizeof(Myclass):4       (class)
sizeof(myclass):4       (obj of class)
虽然新增了static int的成员,但是类/对象的大小未变/依然是4

接下来查看内存布局

#include <iostream>

class Myclass
{
public:
    int m_a;
    char m_b;
    int m_c;
    char * str = "abcd.";
    double m_d;
    
    static int m_s;
    void func()
    {
        int local = 0;
    }
};
int Myclass::m_s = 0;

void test01()
{
    std::cout<<"sizeof(Myclass):"<<sizeof(Myclass)<<"\t(class)\n";
    Myclass myclass;
    std::cout<<"sizeof(myclass):"<<sizeof(myclass)<<"\t(obj of class)\n";
    
}



int main()
{
    test01();

    return EXIT_SUCCESS;
}

调试查看成员的地址:
在这里插入图片描述

WATCH中的值为:

1. &myclass = 0x7fffffffdf410

2. &myclass.m_a = 0x7fffffffdf410

3. &myclass.m_b = 0x7fffffffdf414 "cxx."

4. &myclass.m_c = 0x7fffffffdf418

5. &myclass.str = 0x7fffffffdf420

6. &myclass.m_d = 0x7fffffffdf428

7. &myclass.m_s = 0x55555558154 <Myclass::m_s>

从地址&myclass.m_s = 0x55555558154可以看出,static类型的成员是放在静态区的,和其他成员变量的地址有明显的区别


类对象的声明

对于以下的Person类:


class Person{

public:

    Person()
    {
        std::cout<<"call Person()\n";
    }
    Person(int age):age(0)
    {
        std::cout<<"call Person(int age)\n";

        this->age = age;
    }
    Person(const Person &person)
    {
        std::cout<<"call Person(cosnt Person&person)\n";
        
        age = person.age;
    }

    
    int age;

};

Person p1 = Person(18); 

//这个语句是如何工作的?是先调用有参构造构造出一个匿名的对象吗?然后再调用重载的赋值运算符,将这个匿名对象赋值给p1?
//会不会涉及到拷贝构造函数呢?
//还是说编译器根本没有构造一个匿名对象,而是直接把这个语句当做 “Person p1(18);”?
//又或者是这样做的但是编译器进行了优化?


一、这句代码的标准含义

Person p1 = Person(18);

这是**拷贝初始化(copy initialization)**的语法。

  • 先调用 Person(int) 构造函数,构造一个匿名的临时对象
  • 然后使用该匿名对象调用拷贝构造函数初始化 p1
  • 注意这里不是赋值运算符,是拷贝构造函数,因为 p1新声明的对象

二、优化情况(返回值优化)

现代编译器通常会执行返回值优化(RVO)拷贝消除(copy elision)
这导致实际上不会生成临时对象,而是直接构造 p1
因此:

  • 编译器可能直接用 Person(int) 构造函数初始化 p1
  • 不调用拷贝构造函数,
  • 也没有匿名对象拷贝过程,效率更高。

三、总结

步骤发生与否(标准情况)现代编译器优化后
调用有参构造函数
构造匿名临时对象
调用拷贝构造函数
调用赋值运算符

四、不同声明对象方式举例和详细分析

赋值法(先声明,后赋值)
Person p4;
p4 = Person(18);
  • 先调用默认构造函数 Person() 构造 p4
  • 调用有参构造函数构造匿名临时对象 Person(18)
  • 调用重载的赋值运算符进行赋值(注意你的类没有自定义赋值运算符,会用编译器默认的)。
  • 最终 p4 的内容为临时对象的内容。
  • 输出:
call Person()
call Person(int age)
统一初始化列表法(C++11)
Person p5{18};
  • 直接调用 Person(int) 构造函数初始化 p5
  • 不涉及临时对象、拷贝构造。
  • 输出:
call Person(int age)
隐式转换法
void foo(Person p) { }
foo(18);  // 18 隐式转换成 Person(18)
  • 调用 Person(int) 构造临时匿名对象传参。
  • 可能调用拷贝构造函数拷贝参数,或者优化消除。
  • 输出:
call Person(int age)
call Person(copy constructor)  // 可能调用,也可能优化

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值