Con trỏ tới thành viên dữ liệu của lớp


242

Tôi đã xem qua đoạn mã kỳ lạ này sẽ biên dịch tốt:

class Car
{
    public:
    int speed;
};

int main()
{
    int Car::*pSpeed = &Car::speed;
    return 0;
}

Tại sao C ++ có con trỏ này đến một thành viên dữ liệu không tĩnh của một lớp? Việc sử dụng con trỏ lạ này trong mã thực là gì?


Đây là nơi tôi tìm thấy nó, làm tôi bối rối quá ... nhưng bây giờ có ý nghĩa: stackoverflow.com/a/982941/211160
HostileFork nói không tin tưởng SE

Câu trả lời:


188

Đó là "con trỏ tới thành viên" - đoạn mã sau minh họa việc sử dụng nó:

#include <iostream>
using namespace std;

class Car
{
    public:
    int speed;
};

int main()
{
    int Car::*pSpeed = &Car::speed;

    Car c1;
    c1.speed = 1;       // direct access
    cout << "speed is " << c1.speed << endl;
    c1.*pSpeed = 2;     // access via pointer to member
    cout << "speed is " << c1.speed << endl;
    return 0;
}

Về lý do tại sao bạn muốn làm điều đó, nó sẽ cung cấp cho bạn một mức độ gián tiếp khác có thể giải quyết một số vấn đề khó khăn. Nhưng thành thật mà nói, tôi chưa bao giờ phải sử dụng chúng trong mã của riêng mình.

Chỉnh sửa: Tôi không thể nghĩ ra cách sử dụng thuyết phục cho con trỏ vào dữ liệu thành viên. Con trỏ tới các hàm thành viên có thể được sử dụng trong các kiến ​​trúc có thể cắm được, nhưng một lần nữa tạo ra một ví dụ trong một không gian nhỏ đánh bại tôi. Sau đây là thử tốt nhất (chưa được kiểm tra) của tôi - một chức năng Áp dụng sẽ thực hiện một số xử lý trước & sau khi áp dụng chức năng thành viên do người dùng chọn cho một đối tượng:

void Apply( SomeClass * c, void (SomeClass::*func)() ) {
    // do hefty pre-call processing
    (c->*func)();  // call user specified function
    // do hefty post-call processing
}

Các dấu ngoặc đơn xung quanh c->*funclà cần thiết bởi vì ->*toán tử có độ ưu tiên thấp hơn toán tử gọi hàm.


3
Bạn có thể chỉ ra một ví dụ về một tình huống khó khăn trong đó điều này hữu ích? Cảm ơn.
Ashwin Nanjappa

Tôi có một ví dụ về việc sử dụng con trỏ tới thành viên trong lớp Traits trong một câu trả lời SO khác .
Mike DeSimone

Một ví dụ là viết một lớp "gọi lại" cho một số hệ thống dựa trên sự kiện. Ví dụ, hệ thống đăng ký sự kiện UI của CEGUI có một cuộc gọi lại theo khuôn mẫu lưu trữ một con trỏ tới hàm thành viên bạn chọn, để bạn có thể chỉ định một phương thức để xử lý sự kiện.
Benji XVI

2
Có một ví dụ khá hay về việc sử dụng con trỏ- dữ liệu -member trong một hàm mẫu trong mã này
alveko

3
Gần đây tôi đã sử dụng các con trỏ tới các thành viên dữ liệu trong khung tuần tự hóa. Đối tượng marshaller tĩnh được khởi tạo với danh sách các hàm bao chứa con trỏ tới các thành viên dữ liệu tuần tự hóa. Một nguyên mẫu ban đầu của mã này.
Alexey Biryukov

79

Đây là ví dụ đơn giản nhất mà tôi có thể nghĩ về việc chuyển tải các trường hợp hiếm hoi có tính năng này thích hợp:

#include <iostream>

class bowl {
public:
    int apples;
    int oranges;
};

int count_fruit(bowl * begin, bowl * end, int bowl::*fruit)
{
    int count = 0;
    for (bowl * iterator = begin; iterator != end; ++ iterator)
        count += iterator->*fruit;
    return count;
}

int main()
{
    bowl bowls[2] = {
        { 1, 2 },
        { 3, 5 }
    };
    std::cout << "I have " << count_fruit(bowls, bowls + 2, & bowl::apples) << " apples\n";
    std::cout << "I have " << count_fruit(bowls, bowls + 2, & bowl::oranges) << " oranges\n";
    return 0;
}

Điều cần lưu ý ở đây là con trỏ được truyền vào Count_fruit. Điều này giúp bạn tiết kiệm khi phải viết các hàm Count_apples và Count_oranges riêng biệt.


3
Không nên &bowls.apples&bowls.orangeskhông? &bowl::apples&bowl::orangeskhông chỉ ra bất cứ điều gì.
Dan Nissenbaum

19
&bowl::apples&bowl::orangeskhông chỉ vào các thành viên của một đối tượng ; họ chỉ vào các thành viên của một lớp . Chúng cần được kết hợp với một con trỏ tới một đối tượng thực tế trước khi chúng trỏ đến một cái gì đó. Sự kết hợp đó đạt được với ->*nhà điều hành.
John McFarlane

58

Một ứng dụng khác là danh sách xâm nhập. Kiểu phần tử có thể cho biết danh sách các con trỏ tiếp theo / trước của nó là gì. Vì vậy, danh sách không sử dụng tên được mã hóa cứng nhưng vẫn có thể sử dụng các con trỏ hiện có:

// say this is some existing structure. And we want to use
// a list. We can tell it that the next pointer
// is apple::next.
struct apple {
    int data;
    apple * next;
};

// simple example of a minimal intrusive list. Could specify the
// member pointer as template argument too, if we wanted:
// template<typename E, E *E::*next_ptr>
template<typename E>
struct List {
    List(E *E::*next_ptr):head(0), next_ptr(next_ptr) { }

    void add(E &e) {
        // access its next pointer by the member pointer
        e.*next_ptr = head;
        head = &e;
    }

    E * head;
    E *E::*next_ptr;
};

int main() {
    List<apple> lst(&apple::next);

    apple a;
    lst.add(a);
}

Nếu đây thực sự là một danh sách được liên kết, bạn sẽ không muốn một cái gì đó như thế này: void add (E * e) {e -> * next_ptr = head; đầu = e; } ??
eeeeaaii

4
@eee Tôi khuyên bạn nên đọc về tham số tham khảo. Những gì tôi đã làm về cơ bản là tương đương với những gì bạn đã làm.
Julian Schaub - litb

+1 cho ví dụ mã của bạn, nhưng tôi không thấy cần thiết cho việc sử dụng con trỏ đến thành viên, ví dụ nào khác?
Alcott

3
@Alcott: Bạn có thể áp dụng nó cho các cấu trúc giống như danh sách liên kết khác, nơi con trỏ tiếp theo không được đặt tên next.
icktoofay

41

Đây là một ví dụ thực tế mà tôi đang làm việc ngay bây giờ, từ các hệ thống xử lý / điều khiển tín hiệu:

Giả sử bạn có một số cấu trúc đại diện cho dữ liệu bạn đang thu thập:

struct Sample {
    time_t time;
    double value1;
    double value2;
    double value3;
};

Bây giờ giả sử rằng bạn nhét chúng vào một vectơ:

std::vector<Sample> samples;
... fill the vector ...

Bây giờ giả sử rằng bạn muốn tính toán một số hàm (nói trung bình) của một trong các biến qua một phạm vi mẫu và bạn muốn tính toán phép tính trung bình này thành một hàm. Con trỏ thành viên làm cho nó dễ dàng:

double Mean(std::vector<Sample>::const_iterator begin, 
    std::vector<Sample>::const_iterator end,
    double Sample::* var)
{
    float mean = 0;
    int samples = 0;
    for(; begin != end; begin++) {
        const Sample& s = *begin;
        mean += s.*var;
        samples++;
    }
    mean /= samples;
    return mean;
}

...
double mean = Mean(samples.begin(), samples.end(), &Sample::value2);

Lưu ý Đã chỉnh sửa 2016/08/05 cho cách tiếp cận chức năng mẫu ngắn gọn hơn

Và, tất nhiên, bạn có thể tạo mẫu để tính giá trị trung bình cho bất kỳ trình lặp chuyển tiếp và bất kỳ loại giá trị nào hỗ trợ bổ sung với chính nó và chia cho size_t:

template<typename Titer, typename S>
S mean(Titer begin, const Titer& end, S std::iterator_traits<Titer>::value_type::* var) {
    using T = typename std::iterator_traits<Titer>::value_type;
    S sum = 0;
    size_t samples = 0;
    for( ; begin != end ; ++begin ) {
        const T& s = *begin;
        sum += s.*var;
        samples++;
    }
    return sum / samples;
}

struct Sample {
    double x;
}

std::vector<Sample> samples { {1.0}, {2.0}, {3.0} };
double m = mean(samples.begin(), samples.end(), &Sample::x);

EDIT - Đoạn mã trên có ý nghĩa về hiệu suất

Bạn nên lưu ý, như tôi sớm phát hiện ra, đoạn mã trên có một số hàm ý hiệu suất nghiêm trọng. Tóm tắt là nếu bạn đang tính toán một thống kê tóm tắt theo chuỗi thời gian hoặc tính toán FFT, v.v., thì bạn nên lưu trữ các giá trị cho từng biến liên tục trong bộ nhớ. Nếu không, việc lặp lại chuỗi sẽ gây ra lỗi nhớ cache cho mỗi giá trị được truy xuất.

Hãy xem xét hiệu suất của mã này:

struct Sample {
  float w, x, y, z;
};

std::vector<Sample> series = ...;

float sum = 0;
int samples = 0;
for(auto it = series.begin(); it != series.end(); it++) {
  sum += *it.x;
  samples++;
}
float mean = sum / samples;

Trên nhiều kiến ​​trúc, một ví dụ về Sample sẽ điền vào một dòng bộ đệm. Vì vậy, trên mỗi lần lặp của vòng lặp, một mẫu sẽ được kéo từ bộ nhớ vào bộ đệm. 4 byte từ dòng bộ đệm sẽ được sử dụng và phần còn lại bị vứt đi, và lần lặp tiếp theo sẽ dẫn đến một lỗi bộ nhớ cache khác, truy cập bộ nhớ, v.v.

Tốt hơn nhiều để làm điều này:

struct Samples {
  std::vector<float> w, x, y, z;
};

Samples series = ...;

float sum = 0;
float samples = 0;
for(auto it = series.x.begin(); it != series.x.end(); it++) {
  sum += *it;
  samples++;
}
float mean = sum / samples;

Bây giờ khi giá trị x đầu tiên được tải từ bộ nhớ, ba giá trị tiếp theo cũng sẽ được tải vào bộ đệm (giả sử căn chỉnh phù hợp), nghĩa là bạn không cần bất kỳ giá trị nào được tải cho ba lần lặp tiếp theo.

Thuật toán trên có thể được cải thiện phần nào thông qua việc sử dụng các hướng dẫn SIMD trên các kiến ​​trúc SSE2. Tuy nhiên, những công việc này nhiều tốt hơn nếu các giá trị nằm liền kề nhau trong bộ nhớ và bạn có thể sử dụng một lệnh duy nhất để tải bốn mẫu với nhau (nhiều hơn trong các phiên bản SSE sau này).

YMMV - thiết kế cấu trúc dữ liệu của bạn cho phù hợp với thuật toán của bạn.


Thật tuyệt vời. Tôi sắp thực hiện một cái gì đó rất giống nhau, và bây giờ tôi không phải tìm ra cú pháp lạ! Cảm ơn!
Nicu Stiurca

Đây là câu trả lời tốt nhất. Phần double Sample::*chính là chìa khóa!
Mắt

37

Bạn sau đó có thể truy cập vào diễn đàn này, trên bất kỳ ví dụ:

int main()
{    
  int Car::*pSpeed = &Car::speed;    
  Car myCar;
  Car yourCar;

  int mySpeed = myCar.*pSpeed;
  int yourSpeed = yourCar.*pSpeed;

  assert(mySpeed > yourSpeed); // ;-)

  return 0;
}

Lưu ý rằng bạn cần một cá thể để gọi nó, vì vậy nó không hoạt động như một đại biểu.
Nó hiếm khi được sử dụng, tôi cần nó có thể một hoặc hai lần trong tất cả các năm của tôi.

Thông thường sử dụng một giao diện (tức là một lớp cơ sở thuần túy trong C ++) là lựa chọn thiết kế tốt hơn.


Nhưng chắc chắn đây chỉ là thực hành xấu? nên làm một cái gì đó như youcar.setspeed (mycar.getpspeed)
thecoshman

9
@thecoshman: hoàn toàn phụ thuộc - việc ẩn các thành viên dữ liệu đằng sau các phương thức set / get không phải là đóng gói và chỉ đơn thuần là một nỗ lực vắt sữa trong việc trừu tượng hóa giao diện. Trong nhiều kịch bản, "không chuẩn hóa" đối với các thành viên công cộng là một lựa chọn hợp lý. Nhưng cuộc thảo luận đó có lẽ vượt quá giới hạn của chức năng bình luận.
peterchen

4
+1 để chỉ ra, nếu tôi hiểu chính xác, đây là một con trỏ tới một thành viên của bất kỳ trường hợp nào và không phải là một con trỏ tới một giá trị cụ thể của một thể hiện, đó là phần tôi hoàn toàn thiếu.
johnbakers

@Fellowshee Bạn hiểu chính xác :) (nhấn mạnh rằng trong câu trả lời).
peterchen

26

IBM có thêm một số tài liệu về cách sử dụng cái này. Tóm lại, bạn đang sử dụng con trỏ như một phần bù vào lớp. Bạn không thể sử dụng các con trỏ này ngoài lớp mà chúng đề cập đến, vì vậy:

  int Car::*pSpeed = &Car::speed;
  Car mycar;
  mycar.*pSpeed = 65;

Có vẻ hơi mơ hồ, nhưng một ứng dụng có thể là nếu bạn đang cố gắng viết mã để khử lưu trữ dữ liệu chung thành nhiều loại đối tượng khác nhau và mã của bạn cần xử lý các loại đối tượng mà nó hoàn toàn không biết gì (ví dụ: mã của bạn là trong thư viện và các đối tượng bạn giải nén được tạo bởi người dùng thư viện của bạn). Các con trỏ thành viên cung cấp cho bạn một cách chung chung, dễ đọc để đề cập đến các độ lệch thành viên dữ liệu riêng lẻ mà không cần phải sử dụng đến khoảng trống không đánh máy * theo cách bạn có thể cho các cấu trúc C.


Bạn có thể chia sẻ một ví dụ đoạn mã trong đó cấu trúc này hữu ích không? Cảm ơn.
Ashwin Nanjappa

2
Tôi hiện đang làm rất nhiều việc này do thực hiện một số công việc DCOM và sử dụng các lớp tài nguyên được quản lý, bao gồm thực hiện một chút công việc trước mỗi cuộc gọi và sử dụng các thành viên dữ liệu để thể hiện nội bộ để gửi đến com, cộng với việc tạo khuôn mẫu, tạo ra rất nhiều mã nồi hơi nhỏ hơn nhiều
Dan

19

Nó làm cho nó có thể liên kết các biến thành viên và các hàm theo cách thống nhất. Sau đây là ví dụ với lớp Xe của bạn. Việc sử dụng phổ biến hơn sẽ là ràng buộc std::pair::first::secondkhi sử dụng trong các thuật toán STL và Boost trên bản đồ.

#include <list>
#include <algorithm>
#include <iostream>
#include <iterator>
#include <boost/lambda/lambda.hpp>
#include <boost/lambda/bind.hpp>


class Car {
public:
    Car(int s): speed(s) {}
    void drive() {
        std::cout << "Driving at " << speed << " km/h" << std::endl;
    }
    int speed;
};

int main() {

    using namespace std;
    using namespace boost::lambda;

    list<Car> l;
    l.push_back(Car(10));
    l.push_back(Car(140));
    l.push_back(Car(130));
    l.push_back(Car(60));

    // Speeding cars
    list<Car> s;

    // Binding a value to a member variable.
    // Find all cars with speed over 60 km/h.
    remove_copy_if(l.begin(), l.end(),
                   back_inserter(s),
                   bind(&Car::speed, _1) <= 60);

    // Binding a value to a member function.
    // Call a function on each car.
    for_each(s.begin(), s.end(), bind(&Car::drive, _1));

    return 0;
}

11

Bạn có thể sử dụng một mảng con trỏ tới dữ liệu thành viên (đồng nhất) để kích hoạt giao diện kép, có tên thành viên (iexdata) và giao diện mảng (tức là x [idx]).

#include <cassert>
#include <cstddef>

struct vector3 {
    float x;
    float y;
    float z;

    float& operator[](std::size_t idx) {
        static float vector3::*component[3] = {
            &vector3::x, &vector3::y, &vector3::z
        };
        return this->*component[idx];
    }
};

int main()
{
    vector3 v = { 0.0f, 1.0f, 2.0f };

    assert(&v[0] == &v.x);
    assert(&v[1] == &v.y);
    assert(&v[2] == &v.z);

    for (std::size_t i = 0; i < 3; ++i) {
        v[i] += 1.0f;
    }

    assert(v.x == 1.0f);
    assert(v.y == 2.0f);
    assert(v.z == 3.0f);

    return 0;
}

Tôi thường thấy điều này được thực hiện bằng cách sử dụng một liên minh ẩn danh bao gồm một trường mảng v [3] vì điều đó tránh được một sự gián tiếp, nhưng dù sao cũng thông minh và có khả năng hữu ích cho các trường không tiếp giáp.
Dwayne Robinson

2
@DwayneRobinson nhưng sử dụng unionkiểu chơi chữ theo kiểu đó không được tiêu chuẩn cho phép vì nó gọi ra nhiều dạng hành vi không xác định ... trong khi câu trả lời này là ok.
gạch dưới

Đó là một ví dụ gọn gàng nhưng toán tử [] có thể được viết lại mà không cần con trỏ đến thành phần: float *component[] = { &x, &y, &z }; return *component[idx];Tức là, con trỏ đến thành phần dường như không phục vụ mục đích nào ngoại trừ việc che giấu.
tobi_s

2

Một cách tôi đã sử dụng là nếu tôi có hai cách triển khai cách thực hiện một thứ gì đó trong lớp và tôi muốn chọn một cách trong thời gian chạy mà không phải tiếp tục thực hiện một câu lệnh if tức là

class Algorithm
{
public:
    Algorithm() : m_impFn( &Algorithm::implementationA ) {}
    void frequentlyCalled()
    {
        // Avoid if ( using A ) else if ( using B ) type of thing
        (this->*m_impFn)();
    }
private:
    void implementationA() { /*...*/ }
    void implementationB() { /*...*/ }

    typedef void ( Algorithm::*IMP_FN ) ();
    IMP_FN m_impFn;
};

Rõ ràng điều này chỉ thực sự hữu ích nếu bạn cảm thấy mã đang bị cấm đủ để câu lệnh if làm chậm mọi thứ được thực hiện, ví dụ. sâu trong ruột của một số thuật toán chuyên sâu ở đâu đó. Tôi vẫn nghĩ nó thanh lịch hơn câu lệnh if ngay cả trong những tình huống không có công dụng thực tế nhưng đó chỉ là ý kiến ​​của tôi.


Về cơ bản, bạn có thể đạt được điều tương tự với lớp trừu tượng Algorithmvà hai lớp dẫn xuất, ví dụ, AlgorithmAAlgorithmB. Trong trường hợp như vậy, cả hai thuật toán được phân tách tốt và được đảm bảo được kiểm tra độc lập.
shycha

2

Con trỏ đến các lớp không phải con trỏ thực ; một lớp là một cấu trúc logic và không có sự tồn tại vật lý trong bộ nhớ, tuy nhiên, khi bạn xây dựng một con trỏ tới một thành viên của một lớp, nó sẽ bù vào một đối tượng của lớp của thành viên nơi có thể tìm thấy thành viên đó; Điều này đưa ra một kết luận quan trọng: Vì các thành viên tĩnh không được liên kết với bất kỳ đối tượng nào, do đó, một con trỏ tới một thành viên CANNOT trỏ đến một thành viên tĩnh (dữ liệu hoặc hàm) bất cứ điều gì Hãy xem xét những điều sau:

class x {
public:
    int val;
    x(int i) { val = i;}

    int get_val() { return val; }
    int d_val(int i) {return i+i; }
};

int main() {
    int (x::* data) = &x::val;               //pointer to data member
    int (x::* func)(int) = &x::d_val;        //pointer to function member

    x ob1(1), ob2(2);

    cout <<ob1.*data;
    cout <<ob2.*data;

    cout <<(ob1.*func)(ob1.*data);
    cout <<(ob2.*func)(ob2.*data);


    return 0;
}

Nguồn: Toàn bộ tài liệu tham khảo C ++ - Herbert Schildt Phiên bản thứ 4


0

Tôi nghĩ rằng bạn chỉ muốn làm điều này nếu dữ liệu thành viên khá lớn (ví dụ, một đối tượng của một lớp khá lớn khác) và bạn có một số thói quen bên ngoài chỉ hoạt động trên các tham chiếu đến các đối tượng của lớp đó. Bạn không muốn sao chép đối tượng thành viên, vì vậy điều này cho phép bạn vượt qua nó.


0

Dưới đây là một ví dụ nơi con trỏ tới thành viên dữ liệu có thể hữu ích:

#include <iostream>
#include <list>
#include <string>

template <typename Container, typename T, typename DataPtr>
typename Container::value_type searchByDataMember (const Container& container, const T& t, DataPtr ptr) {
    for (const typename Container::value_type& x : container) {
        if (x->*ptr == t)
            return x;
    }
    return typename Container::value_type{};
}

struct Object {
    int ID, value;
    std::string name;
    Object (int i, int v, const std::string& n) : ID(i), value(v), name(n) {}
};

std::list<Object*> objects { new Object(5,6,"Sam"), new Object(11,7,"Mark"), new Object(9,12,"Rob"),
    new Object(2,11,"Tom"), new Object(15,16,"John") };

int main() {
    const Object* object = searchByDataMember (objects, 11, &Object::value);
    std::cout << object->name << '\n';  // Tom
}

0

Giả sử bạn có một cấu trúc. Bên trong cấu trúc đó là * một số loại tên * hai biến cùng loại nhưng có ý nghĩa khác nhau

struct foo {
    std::string a;
    std::string b;
};

Được rồi, bây giờ hãy nói rằng bạn có một bó foos trong một container:

// key: some sort of name, value: a foo instance
std::map<std::string, foo> container;

Được rồi, bây giờ giả sử bạn tải dữ liệu từ các nguồn riêng biệt, nhưng dữ liệu được trình bày theo cùng một kiểu (ví dụ: bạn cần cùng một phương pháp phân tích cú pháp).

Bạn có thể làm một cái gì đó như thế này:

void readDataFromText(std::istream & input, std::map<std::string, foo> & container, std::string foo::*storage) {
    std::string line, name, value;

    // while lines are successfully retrieved
    while (std::getline(input, line)) {
        std::stringstream linestr(line);
        if ( line.empty() ) {
            continue;
        }

        // retrieve name and value
        linestr >> name >> value;

        // store value into correct storage, whichever one is correct
        container[name].*storage = value;
    }
}

std::map<std::string, foo> readValues() {
    std::map<std::string, foo> foos;

    std::ifstream a("input-a");
    readDataFromText(a, foos, &foo::a);
    std::ifstream b("input-b");
    readDataFromText(b, foos, &foo::b);
    return foos;
}

Tại thời điểm này, việc gọi readValues()sẽ trả về một container với sự đồng nhất của "input-a" và "input-b"; tất cả các khóa sẽ có mặt và các foos có a hoặc b hoặc cả hai.


0

Chỉ cần thêm một số trường hợp sử dụng cho câu trả lời của @ anon & @ Oktalist, đây là một tài liệu đọc tuyệt vời về chức năng con trỏ đến thành viên và dữ liệu con trỏ đến thành viên.

https://www.dre.vanderbilt.edu/~schmidt/PDF/C++-ptmf4.pdf


các liên kết đã chết. Đó là lý do tại sao câu trả lời chỉ liên kết không được mong đợi ở đây. Ít nhất là tóm tắt nội dung của liên kết, nếu không câu trả lời của bạn trở nên không hợp lệ khi liên kết
rots
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.