【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关键字及其修饰的静态成员变量/函数详解 的博客能对大家有所帮助,欢迎大佬们留言或私信与我交流.

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



相关文章
|
5月前
|
存储 C++
C++语言中指针变量int和取值操作ptr详细说明。
总结起来,在 C++ 中正确理解和运用 int 类型地址及其相关取值、设定等操纵至关重要且基础性强:定义 int 类型 pointer 需加星号;初始化 pointer 需配合 & 取址;读写 pointer 执向之处需配合 * 解引用操纵进行。
528 12
|
7月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
206 0
|
10月前
|
安全 C++
【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)
本文深入探讨了C++中的继承机制,作为面向对象编程(OOP)的核心特性之一。继承通过允许派生类扩展基类的属性和方法,极大促进了代码复用,增强了代码的可维护性和可扩展性。文章详细介绍了继承的基本概念、定义格式、继承方式(public、protected、private)、赋值兼容转换、作用域问题、默认成员函数规则、继承与友元、静态成员、多继承及菱形继承问题,并对比了继承与组合的优缺点。最后总结指出,虽然继承提高了代码灵活性和复用率,但也带来了耦合度高的问题,建议在“has-a”和“is-a”关系同时存在时优先使用组合。
558 6
|
程序员 C++ 容器
在 C++中,realloc 函数返回 NULL 时,需要手动释放原来的内存吗?
在 C++ 中,当 realloc 函数返回 NULL 时,表示内存重新分配失败,但原内存块仍然有效,因此需要手动释放原来的内存,以避免内存泄漏。
|
11月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
9月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
385 12
|
7月前
|
存储 编译器 程序员
c++的类(附含explicit关键字,友元,内部类)
本文介绍了C++中类的核心概念与用法,涵盖封装、继承、多态三大特性。重点讲解了类的定义(`class`与`struct`)、访问限定符(`private`、`public`、`protected`)、类的作用域及成员函数的声明与定义分离。同时深入探讨了类的大小计算、`this`指针、默认成员函数(构造函数、析构函数、拷贝构造、赋值重载)以及运算符重载等内容。 文章还详细分析了`explicit`关键字的作用、静态成员(变量与函数)、友元(友元函数与友元类)的概念及其使用场景,并简要介绍了内部类的特性。
323 0
|
10月前
|
设计模式 安全 C++
【C++进阶】特殊类设计 && 单例模式
通过对特殊类设计和单例模式的深入探讨,我们可以更好地设计和实现复杂的C++程序。特殊类设计提高了代码的安全性和可维护性,而单例模式则确保类的唯一实例性和全局访问性。理解并掌握这些高级设计技巧,对于提升C++编程水平至关重要。
198 16
|
11月前
|
编译器 C语言 C++
类和对象的简述(c++篇)
类和对象的简述(c++篇)
|
10月前
|
编译器 C++
类和对象(中 )C++
本文详细讲解了C++中的默认成员函数,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载和取地址运算符重载等内容。重点分析了各函数的特点、使用场景及相互关系,如构造函数的主要任务是初始化对象,而非创建空间;析构函数用于清理资源;拷贝构造与赋值运算符的区别在于前者用于创建新对象,后者用于已存在的对象赋值。同时,文章还探讨了运算符重载的规则及其应用场景,并通过实例加深理解。最后强调,若类中存在资源管理,需显式定义拷贝构造和赋值运算符以避免浅拷贝问题。