Tại sao tôi không thể khởi tạo thành viên tĩnh không phải const hoặc mảng tĩnh trong lớp?


116

Tại sao tôi không thể khởi tạo staticthành viên hoặc staticmảng không phải const trong một lớp?

class A
{
    static const int a = 3;
    static int b = 3;
    static const int c[2] = { 1, 2 };
    static int d[2] = { 1, 2 };
};

int main()
{
    A a;

    return 0;
}

trình biên dịch gặp các lỗi sau:

g++ main.cpp
main.cpp:4:17: error: ISO C++ forbids in-class initialization of non-const static member b
main.cpp:5:26: error: a brace-enclosed initializer is not allowed here before ‘{’ token
main.cpp:5:33: error: invalid in-class initialization of static data member of non-integral type const int [2]’
main.cpp:6:20: error: a brace-enclosed initializer is not allowed here before ‘{’ token
main.cpp:6:27: error: invalid in-class initialization of static data member of non-integral type int [2]’

Tôi có hai câu hỏi:

  1. Tại sao tôi không thể khởi tạo staticcác thành viên dữ liệu trong lớp?
  2. Tại sao tôi không thể khởi tạo staticmảng trong lớp, thậm chí cả constmảng?

1
Tôi nghĩ lý do chính là nó rất khó để đi đúng. Về nguyên tắc, bạn có thể làm những gì bạn đang nói, nhưng sẽ có một số tác dụng phụ kỳ lạ. Giống như nếu ví dụ về mảng của bạn được cho phép, thì bạn có thể nhận được giá trị của A :: c [0], nhưng không thể chuyển A :: c cho một hàm vì nó sẽ yêu cầu địa chỉ và thời gian biên dịch hằng số không có địa chỉ. C ++ 11 đã kích hoạt một số điều này bằng cách sử dụng constexpr.
Vaughn Cato

Câu hỏi tuyệt vời và câu trả lời makred. Liên kết đã giúp tôi: msdn.microsoft.com/en-us/library/0e5kx78b.aspx
ETFovac

Câu trả lời:


144

Tại sao tôi không thể khởi tạo staticcác thành viên dữ liệu trong lớp?

Tiêu chuẩn C ++ chỉ cho phép các kiểu liệt kê hoặc tích phân hằng tĩnh được khởi tạo bên trong lớp. Đây là lý do ađược phép khởi tạo trong khi những người khác thì không.

Tham khảo:
C ++ 03 9.4.2 Các thành viên dữ liệu tĩnh
§4

Nếu một phần tử dữ liệu tĩnh thuộc kiểu liệt kê const tích phân hoặc const, thì khai báo của nó trong định nghĩa lớp có thể chỉ định bộ khởi tạo hằng là biểu thức hằng tích phân (5.19). Trong trường hợp đó, phần tử có thể xuất hiện trong các biểu thức hằng số tích phân. Thành viên sẽ vẫn được xác định trong phạm vi không gian tên nếu nó được sử dụng trong chương trình và định nghĩa phạm vi không gian tên sẽ không chứa bộ khởi tạo.

Các loại tích phân là gì?

C ++ 03 3.9.1 Các kiểu cơ bản
§7

Các kiểu bool, char, wchar_t và các kiểu số nguyên có dấu và không dấu được gọi chung là kiểu tích phân.43) Từ đồng nghĩa với kiểu tích phân là kiểu số nguyên.

Chú thích cuối trang:

43) Do đó, các phép liệt kê (7.2) không phải là tích phân; tuy nhiên, các phép liệt kê có thể được thăng cấp thành int, unsigned int, long, hoặc unsigned long, như được chỉ định trong 4.5.

Cách giải quyết:

Bạn có thể sử dụng thủ thuật enum để khởi tạo một mảng bên trong định nghĩa lớp của bạn.

class A 
{
    static const int a = 3;
    enum { arrsize = 2 };

    static const int c[arrsize] = { 1, 2 };

};

Tại sao Tiêu chuẩn không cho phép điều này?

Bjarne giải thích điều này một cách khéo léo ở đây :

Một lớp thường được khai báo trong tệp tiêu đề và tệp tiêu đề thường được đưa vào nhiều đơn vị dịch. Tuy nhiên, để tránh các quy tắc liên kết phức tạp, C ++ yêu cầu mọi đối tượng phải có một định nghĩa duy nhất. Quy tắc đó sẽ bị phá vỡ nếu C ++ cho phép định nghĩa trong lớp của các thực thể cần được lưu trữ trong bộ nhớ dưới dạng đối tượng.

Tại sao chỉ static constcác kiểu tích phân & enums mới được phép Khởi tạo trong lớp?

Câu trả lời ẩn trong câu trích dẫn của Bjarne, hãy đọc kỹ,
"C ++ yêu cầu mọi đối tượng phải có một định nghĩa duy nhất. Quy tắc đó sẽ bị phá vỡ nếu C ++ cho phép định nghĩa trong lớp về các thực thể cần được lưu trữ trong bộ nhớ dưới dạng đối tượng."

Lưu ý rằng chỉ static constsố nguyên mới có thể được coi là hằng số thời gian biên dịch. Trình biên dịch biết rằng giá trị số nguyên sẽ không thay đổi bất cứ lúc nào và do đó nó có thể áp dụng phép thuật của riêng mình và áp dụng tối ưu hóa, trình biên dịch chỉ cần nội dòng các thành viên lớp đó tức là chúng không được lưu trữ trong bộ nhớ nữa, vì nhu cầu được lưu trữ trong bộ nhớ bị loại bỏ , nó cung cấp cho các biến như vậy ngoại lệ đối với quy tắc được Bjarne đề cập.

Điều cần lưu ý ở đây là ngay cả khi static constcác giá trị tích phân có thể có Khởi tạo trong lớp, thì việc lấy địa chỉ của các biến như vậy không được phép. Người ta có thể lấy địa chỉ của một thành viên tĩnh nếu (và chỉ khi) nó có một định nghĩa ngoài lớp. Điều này càng xác nhận lý do ở trên.

enums được cho phép điều này vì các giá trị của một kiểu liệt kê có thể được sử dụng khi int được mong đợi. xem trích dẫn ở trên


Điều này thay đổi như thế nào trong C ++ 11?

C ++ 11 nới lỏng hạn chế ở một mức độ nhất định.

C ++ 11 9.4.2 Các thành viên dữ liệu tĩnh
§3

Nếu một thành viên dữ liệu tĩnh có kiểu ký tự const, khai báo của nó trong định nghĩa lớp có thể chỉ định một bộ khởi tạo dấu ngoặc nhọn hoặc dấu bằng trong đó mọi bộ khởi tạo-mệnh đề là một biểu thức gán là một biểu thức hằng. Một thành viên dữ liệu tĩnh của kiểu chữ có thể được khai báo trong định nghĩa lớp với constexpr specifier;if như vậy, khai báo của nó sẽ chỉ định một bộ khởi tạo dấu ngoặc nhọn hoặc dấu bằng trong đó mỗi bộ khởi tạo-mệnh đề là một biểu thức gánlà một biểu thức hằng. [Lưu ý: Trong cả hai trường hợp này, thành viên có thể xuất hiện trong các biểu thức không đổi. —End note] Thành viên sẽ vẫn được xác định trong phạm vi không gian tên nếu nó được sử dụng trong chương trình và định nghĩa phạm vi không gian tên sẽ không chứa trình khởi tạo.

Ngoài ra, C ++ 11 sẽ cho phép (§12.6.2.8) một thành viên dữ liệu không tĩnh được khởi tạo ở nơi nó được khai báo (trong lớp của nó). Điều này có nghĩa là ngữ nghĩa người dùng dễ dàng hơn nhiều.

Lưu ý rằng các tính năng này chưa được triển khai trong gcc 4.7 mới nhất, Vì vậy, bạn vẫn có thể gặp lỗi biên dịch.


7
Mọi thứ khác trong c ++ 11. Câu trả lời có thể sử dụng cập nhật.
bames53,

4
Điều này có vẻ không đúng: "Lưu ý rằng chỉ các số nguyên hằng số tĩnh mới có thể được coi là hằng số thời gian biên dịch. Trình biên dịch biết rằng giá trị số nguyên sẽ không thay đổi bất cứ lúc nào và do đó nó có thể áp dụng phép thuật của riêng mình và áp dụng các tối ưu hóa, trình biên dịch đơn giản nội dòng các thành viên lớp như vậy tức là chúng không được lưu trong bộ nhớ nữa , " Bạn có chắc là chúng không nhất thiết được lưu trong bộ nhớ không? Nếu tôi cung cấp định nghĩa cho các thành viên thì sao? Điều gì sẽ &membertrở lại?
Nawaz

2
@Als: Vâng. Đó là những gì câu hỏi của tôi là. Vậy tại sao C ++ chỉ cho phép khởi tạo trong lớp cho các kiểu tích phân, không được bạn trả lời chính xác. Hãy nghĩ xem tại sao nó không cho phép khởi tạo static const char*thành viên?
Nawaz

3
@Nawaz: Bởi vì C ++ 03 chỉ cho phép bộ khởi tạo hằng số cho kiểu liệt kê tĩnh và tích phân const và không có kiểu nào khác, nên C ++ 11 mở rộng điều này thành một kiểu ký tự const , điều này làm giảm các tiêu chuẩn cho Khởi tạo trong lớp. trong C ++ 03 có lẽ là một sự giám sát đảm bảo một sự thay đổi và do đó đã được sửa chữa trong C ++ 11, nếu có bất kỳ lý do chiến thuật truyền thống nào cho sự thay đổi, tôi không biết về chúng. Nếu bạn biết bất kỳ điều gì, vui lòng chia sẻ chúng.
Alok Save

4
Mệnh đề "Giải pháp thay thế" bạn đã đề cập không hoạt động với g ++.
iammilind

4

Điều này dường như dựa vào ngày xưa của các trình liên kết đơn giản. Bạn có thể sử dụng các biến tĩnh trong các phương thức tĩnh để giải quyết:

// header.hxx
#include <vector>

class Class {
public:
    static std::vector<int> & replacement_for_initialized_static_non_const_variable() {
        static std::vector<int> Static {42, 0, 1900, 1998};
        return Static;
    }
};

int compilation_unit_a();

// compilation_unit_a.cxx
#include "header.hxx"

int compilation_unit_a() {  
    return Class::replacement_for_initialized_static_non_const_variable()[1]++;
}

// main.cxx
#include "header.hxx"

#include <iostream>

int main() {
    std::cout
    << compilation_unit_a()
    << Class::replacement_for_initialized_static_non_const_variable()[1]++
    << compilation_unit_a()
    << Class::replacement_for_initialized_static_non_const_variable()[1]++
    << std::endl;
}

xây dựng:

g++ -std=gnu++0x -save-temps=obj -c compilation_unit_a.cxx 
g++ -std=gnu++0x -o main main.cxx compilation_unit_a.o

chạy:

./main

Thực tế là điều này hoạt động (nhất quán, ngay cả khi định nghĩa lớp được bao gồm trong các đơn vị biên dịch khác nhau), cho thấy rằng trình liên kết ngày nay (gcc 4.9.2) thực sự đủ thông minh.

Hài hước: In 0123trên cánh tay và 3210trên x86.


1

Tôi nghĩ rằng nó để ngăn bạn trộn các khai báo và định nghĩa. (Hãy nghĩ về các vấn đề có thể xảy ra nếu bạn đưa tệp vào nhiều nơi.)


0

Đó là bởi vì chỉ có thể có một định nghĩa về A::atất cả các đơn vị dịch thuật sử dụng.

Nếu bạn thực hiện static int a = 3;trong một lớp trong tiêu đề được bao gồm trong tất cả các đơn vị dịch thì bạn sẽ nhận được nhiều định nghĩa. Do đó, định nghĩa không nằm ngoài dòng của một tĩnh buộc phải tạo ra lỗi trình biên dịch.

Sử dụng static inlinehoặc static constkhắc phục điều này. static inlinechỉ cụ thể hóa biểu tượng nếu nó được sử dụng trong đơn vị dịch và đảm bảo trình liên kết chỉ chọn và để lại một bản sao nếu nó được xác định trong nhiều đơn vị dịch do nó nằm trong một nhóm comdat. constở phạm vi tệp làm cho trình biên dịch không bao giờ phát ra một ký hiệu vì nó luôn được thay thế ngay lập tức trong mã trừ khi externđược sử dụng, điều này không được phép trong một lớp.

Một điều cần lưu ý là static inline int b;được coi như một định nghĩa trong khi static const int bhoặc static const A b;vẫn được coi như một khai báo và phải được định nghĩa ngoài dòng nếu bạn không định nghĩa nó bên trong lớp. Điều thú vị static constexpr A b;là được coi như một định nghĩa, trong khi đó static constexpr int b;là một lỗi và phải có một bộ khởi tạo (điều này là do chúng bây giờ trở thành định nghĩa và giống như bất kỳ định nghĩa const / constexpr nào ở phạm vi tệp, chúng yêu cầu một bộ khởi tạo mà int không có nhưng một loại lớp bởi vì nó có một ẩn = A()khi nó là một định nghĩa - clang cho phép điều này nhưng gcc yêu cầu bạn khởi tạo rõ ràng hoặc đó là một lỗi. Thay vào đó, đây không phải là vấn đề với nội tuyến). static const A b = A();không được phép và phải constexprhoặcinlineđể cho phép một trình khởi tạo cho một đối tượng tĩnh với kiểu lớp, tức là tạo một thành viên tĩnh của kiểu lớp hơn một khai báo. Vì vậy, có trong một số tình huống nhất định A a;không giống như khởi tạo rõ ràng A a = A();(cái trước có thể là một khai báo nhưng nếu chỉ cho phép một khai báo cho kiểu đó thì cái sau là lỗi. Cái sau chỉ có thể được sử dụng trên một định nghĩa. Làm constexprcho nó trở thành một định nghĩa ). Nếu bạn sử dụng constexprvà chỉ định một hàm tạo mặc định thì hàm tạo sẽ cầnconstexpr

#include<iostream>

struct A
{
    int b =2;
    mutable int c = 3; //if this member is included in the class then const A will have a full .data symbol emitted for it on -O0 and so will B because it contains A.
    static const int a = 3;
};

struct B {
    A b;
    static constexpr A c; //needs to be constexpr or inline and doesn't emit a symbol for A a mutable member on any optimisation level
};

const A a;
const B b;

int main()
{
    std::cout << a.b << b.b.b;
    return 0;
}

Thành viên tĩnh là một khai báo phạm vi tệp hoàn toàn extern int A::a;(chỉ có thể được thực hiện trong lớp và các định nghĩa ngoài dòng phải tham chiếu đến thành viên tĩnh trong một lớp và phải là định nghĩa và không được chứa extern) trong khi thành viên không tĩnh là một phần của định nghĩa kiểu hoàn chỉnh của một lớp và có các quy tắc giống như khai báo phạm vi tệp mà không có extern. Chúng là những định nghĩa ngầm định. Định int i[]; int i[5];nghĩa lại cũng vậy, trong khi static int i[]; int A::i[5];không giống như 2 externs, trình biên dịch sẽ vẫn phát hiện một thành viên trùng lặp nếu bạn làm static int i[]; static int i[5];trong lớp.


-3

các biến tĩnh dành riêng cho một lớp. Các trình xây dựng khởi tạo các thuộc tính ĐẶC BIỆT cho một thể hiện.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.