【C++】static关键字及其修饰的静态成员变量/函数详解

简介: 【C++】static关键字及其修饰的静态成员变量/函数详解

什么是static?

static的引入

static 是 C/C++ 中很常用的修饰符,它被用来控制变量的存储方式和可见性


静态数据的存储

全局(静态)存储区

       全局(静态)存储区内存区域中的划分,如下图所示:

      全局(静态)存储区:分为data段和bass段。data段(全局初始化区)存放初始化的全局变量和静态变量;bass段(全局未初始化区)存放未初始化的全局变量和静态变量。程序运行结束时自动释放。其中bass段在程序执行之前会被系统自动清0,所以未初始化的全局变量和静态变量在程序执行之前已经为0。存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。

       在 C++ 中 static 的内部实现机制:静态数据成员要在程序一开始运行时就必须存在。因为函数在程序运行中被调用,所以静态数据成员不能在任何函数内分配空间和初始化。

       这样,它的空间分配有三个可能的地方,一是作为类的外部接口的头文件,那里有类声明;二是类定义的内部实现,那里有类的成员函数定义;三是应用程序的 main() 函数前的全局数据声明和定义处。

       静态数据成员要实际地分配空间,故不能在类的声明中定义(只能声明数据成员)。类声明只声明一个类的"尺寸和规格",并不进行实际的内存分配,所以在类声明中写成定义是错误的。它也不能在头文件中类声明的外部定义,因为那会造成在多个使用该类的源文件中,对其重复定义。

       static 被引入以告知编译器,将变量存储在程序的静态存储区而非栈上空间,静态数据成员按定义出现的先后顺序依次初始化,注意静态成员嵌套时,要保证所嵌套的成员已经初始化了。消除时的顺序是初始化的反顺序。

优势:

       可以节省内存,因为它是所有对象所公有的,因此,对多个对象来说,静态数据成员只存储一处,供所有对象共用。静态数据成员的值对每个对象都是一样,但它的值是可以更新的。只要对静态数据成员的值更新一次,保证所有对象存取更新后的相同的值,这样可以提高时间效率。


static成员概念

       声明为static的类成员称为类的静态成员用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数静态成员变量一定要在类外进行初始化。


static成员特性

  1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区。如下代码:
class Student
{
public:
  Student(const char name[], int idea, int grade)
  {
    strcpy(_name, name);
    _idea = idea;
    _grade = grade;
  }
  static int GetPostalCode()
  {
    return _postalCode;
  }
private:
  char _name[10];
  int _idea;
  int _grade;
 
  static int _postalCode;
};
int Student::_postalCode = 710400;
 
int main()
{
  cout << Student::GetPostalCode() << endl;
 
  Student s1("张三", 1001, 3);
  Student s2("李四", 1002, 2);
  Student s3("王五", 1003, 1);
 
  return 0;
}
  1. 我们通过监控可以发现,在类里成员变量位置定义的静态成员变量并不存在于类对象中: 也就是说,无论开辟了多少类对象,静态成员变量都只有一个,并且不属于任何类对象本身,只有成员变量才属于类对象。静态成员变量和类对象和其成员变量关系如下图:
  2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
  3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
  4. 静态成员函数没有隐藏的this指针不能访问任何非静态成员;但非静态成员可以访问静态成员函数
  5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制

ststic成员的应用

利用static实现一个可以计算程序中正在使用的类对象有多少的类

       我们可以利用对象创建必调用构造,而销毁必调用析构函数的特性,在类里创建一个static类对象来记录类对象的创建数/销毁数。注意,由于全局变量的不安全性,我们并不推荐使用全局变量来完成这项任务,如下代码,定义了一个可以计算程序中有多少类对象还在生命周期的类:

#include<iostream>
using namespace std;
 
class Count
{
public:
  //构造函数,每构造一个对象,scount+1
  Count() { ++_scount; }
 
  //const构造函数,每构造一个对象,scount+1
  Count(const Count& t) { ++_scount; }
 
  //析构函数,每析构一个对象,scount-1
  ~Count() { --_scount; }
 
  //获取scount的值
  static int GetSCount() { return _scount; }
 
private:
  //变量_scount的声明
  static int _scount;
};
//变量_scount的定义
int Count::_scount = 0;
 
//创建全局Count对象a0
Count a0;
 
int main()
{
  cout << __LINE__ << ":" << Count::GetSCount() << endl;
  Count a1, a2;
 
  {
    Count a3(a1);
    cout << __LINE__ << ":" << Count::GetSCount() << endl;
  }//出了域作用限定范围a3的生命周期结束就自动析构了
  
  cout << __LINE__ << ":" << Count::GetSCount() << endl;
 
  return 0;
}

       我们测试一下这段代码是否可以统计当前行有多少个类对象正在使用:

       综上,对于类对象的创建数/销毁数的记录工作,可以从下面三个方向入手:

  • 类对象的创建数=构造函数静态成员变量++
  • 类对象的销毁数=析构函数静态成员变量++
  • 类对象的在生命周期数=构造函数静态成员变量-析构函数静态成员变量

设计一个类,在类外面只能在栈/只能在堆上创建对象

       如下,我们平常创建类对象的时候,如果不加以限制,则类对象可能被创建在不同的内存区域:

class A
{
public:
  A()
  {}
private:
  int _a1 = 1;
  int _a2 = 2;
};
 
int main()
{
  static A aa1;    //类对象在静态区
  A aa2;           //类对象在栈
  A* ptr = new A;  //类对象在堆
 
  return 0;
}

       但假如我们遇到了某种场景,即我们创建的这个类,只希望它在栈上创建对象/只希望它在堆上创建对象时,我们就可以通过将构造函数封装起来,再通过static修饰的类成员函数来创建指定的类对象,如:

class A
{
public:
  static A GetStackObj()
  {
    A aa;
    return aa;
  }
  static A* GetHeapObj()
  {
    return new A;
  }
private:
  A()//构造函数私有化
  {}
private:
  int _a1 = 1;
  int _a2 = 2;
};
 
int main()
{
  //static A aa1;    //类对象在静态区
  //A aa2;           //类对象在栈
  //A* ptr = new A;  //类对象在堆
 
  A::GetStackObj();
  A::GetHeapObj();
 
  return 0;
}

       这里有几点需要解释一下:

       1.将构造函数封装起来是为了不让类外的函数随便不按要求构造类对象,如:

      2.使用成员函数来创建类对象是因为成员函数调用类函数不受访问限定符的限制,如:

      3.使用static修饰成员函数是因为要解决无类对象就无法调用类成员函数的问题,如:

       做个梗图给大家形象理解一下这里的矛盾逻辑:

       综上,巧用封装和static就可以达到一些特殊的我们想实现的效果,要灵活使用啊。


static成员妙解求1+2+3+...+n问题

一.题目描述

牛客网题目链接:
JZ64 求1+2+3+...+n

https://www.nowcoder.com/practice/7a0da8fc483247ff8800059e12d7caf1?tpId=13&tqId=11200&tPage=3&rp=3&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking

描述:

求1+2+3+...+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。

数据范围: 0<n≤200

进阶: 空间复杂度 O(1) ,时间复杂度 O(n)

示例1:

输入:5

返回值:15

示例2:

输入:1

返回值:1

题目详情:


二.题目思路

  • 首先,我们创建一个Sum类,其中包含两个静态成员变量,一个是_i,一个是_ret
  • 其次,我们在主函数创建一个n个Sum类数据的数组,这意味着将要创建n个Sum类对象,则Sum的构造函数会被调用n次
  • 最后,我们在Sum的构造函数里让_ret+=_i后让_i++,这样,创建一个类对象_ret就会加等它的次序,即从1一直加到n。

三.解题代码

       根据上述思路,本题解题代码如下:

class Sum {
public:
    Sum()
    {
        _ret+=_i;
        _i++;
    }
    static int Getret()
    {
        return _ret;
    }
    private:
    static int _i;
    static int _ret;
};
int Sum::_i = 1;
int Sum::_ret = 0;
 
class Solution {
public:
    int Sum_Solution(int n) {
    Sum a[n];//创建了n个对象,调用了n次构造函数
    return Sum::Getret();
    }
};

       拷贝到牛客网测试运行:

       成功通过:


结语

希望这篇关于 static关键字及其修饰的静态成员变量/函数详解 的博客能对大家有所帮助,欢迎大佬们留言或私信与我交流.

学海漫浩浩,我亦苦作舟!关注我,大家一起学习,一起进步!



相关文章
|
1月前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
83 5
|
2月前
|
程序员 C++ 容器
在 C++中,realloc 函数返回 NULL 时,需要手动释放原来的内存吗?
在 C++ 中,当 realloc 函数返回 NULL 时,表示内存重新分配失败,但原内存块仍然有效,因此需要手动释放原来的内存,以避免内存泄漏。
|
2月前
|
存储 前端开发 C++
C++ 多线程之带返回值的线程处理函数
这篇文章介绍了在C++中使用`async`函数、`packaged_task`和`promise`三种方法来创建带返回值的线程处理函数。
79 6
|
2月前
|
C++
C++ 多线程之线程管理函数
这篇文章介绍了C++中多线程编程的几个关键函数,包括获取线程ID的`get_id()`,延时函数`sleep_for()`,线程让步函数`yield()`,以及阻塞线程直到指定时间的`sleep_until()`。
37 0
C++ 多线程之线程管理函数
|
2月前
|
C语言 C++
实现两个变量值的互换[C语言和C++的区别]
实现两个变量值的互换[C语言和C++的区别]
26 0
|
24天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
38 2
|
1月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
80 4
|
1月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
86 4
|
2月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
31 4
|
2月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
32 4